Compare commits
	
		
			151 Commits
		
	
	
		
			bugfix-max
			...
			org-to-pro
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 16aa6672fc | ||
|   | ac99c8e0f7 | ||
|   | df121db78c | ||
|   | 816b41adad | ||
|   | f09dfe18be | ||
|   | 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 | ||
|   | 26b6fa4f0c | ||
|   | 807665fdc1 | ||
|   | d6597d2c8a | ||
|   | 566d67bf48 | ||
|   | d4fb8b689a | ||
|   | 98b231c8bd | ||
|   | 45afb1f1f4 | ||
|   | 2bffb03766 | ||
|   | 223b990005 | ||
|   | fa61c9c472 | ||
|   | 1309a6ec5d | ||
|   | 17a6fd31a5 | ||
|   | e1cbeccb90 | ||
|   | d6b97b29f7 | ||
|   | 09140f8b5f | ||
|   | 9952dd93d8 | ||
|   | e0b457c6c5 | ||
|   | 0c37506975 | ||
|   | 2b2e0ab8ee | ||
|   | 3dbb06ec00 | ||
|   | 85d42a014b | ||
|   | 7d1ded3b18 | ||
|   | b00f6dd04b | ||
|   | 2e395e4d39 | ||
|   | 4b06d05908 | ||
|   | aabf355b81 | ||
|   | 61e5f0775d | ||
|   | cc1d1178da | ||
|   | 7466db63df | ||
|   | 79a0b03bf8 | ||
|   | 6fb7a82d72 | ||
|   | 4ea30a3ba3 | ||
|   | 52d1d5c7ee | ||
|   | 46036a44d2 | ||
|   | 3753fe5c16 | 
| @@ -6,6 +6,10 @@ on: | ||||
|   push: | ||||
|     branches: [main] | ||||
| 
 | ||||
| defaults: | ||||
|   run: | ||||
|     working-directory: app | ||||
| 
 | ||||
| jobs: | ||||
|   run-checks: | ||||
|     runs-on: ubuntu-latest | ||||
| @@ -1,2 +0,0 @@ | ||||
| src/codegen/openai.schema.json | ||||
| pnpm-lock.yaml | ||||
							
								
								
									
										6
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,6 +0,0 @@ | ||||
| { | ||||
|   "eslint.format.enable": true, | ||||
|   "editor.codeActionsOnSave": { | ||||
|     "source.fixAll.eslint": true | ||||
|   } | ||||
| } | ||||
							
								
								
									
										78
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,49 +1,61 @@ | ||||
| <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 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 | ||||
|  | ||||
| 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) | ||||
| - [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) | ||||
|  | ||||
| <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 | ||||
|  | ||||
| OpenPipe currently supports GPT-3.5 and GPT-4. Wider model support is planned. | ||||
| - 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). | ||||
| - 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 | ||||
|  | ||||
|   | ||||
| @@ -26,6 +26,11 @@ NEXT_PUBLIC_SOCKET_URL="http://localhost:3318" | ||||
| NEXTAUTH_SECRET="your_secret" | ||||
| NEXTAUTH_URL="http://localhost:3000" | ||||
| 
 | ||||
| NEXT_PUBLIC_HOST="http://localhost:3000" | ||||
| 
 | ||||
| # Next Auth Github Provider | ||||
| GITHUB_CLIENT_ID="your_client_id" | ||||
| GITHUB_CLIENT_SECRET="your_secret" | ||||
| 
 | ||||
| OPENPIPE_BASE_URL="http://localhost:3000/api" | ||||
| OPENPIPE_API_KEY="your_key" | ||||
| @@ -37,6 +37,7 @@ const config = { | ||||
|       "warn", | ||||
|       { vars: "all", varsIgnorePattern: "^_", args: "after-used", argsIgnorePattern: "^_" }, | ||||
|     ], | ||||
|     "react/no-unescaped-entities": "off", | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
							
								
								
									
										3
									
								
								.gitignore → app/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -40,3 +40,6 @@ yarn-error.log* | ||||
| 
 | ||||
| # typescript | ||||
| *.tsbuildinfo | ||||
| 
 | ||||
| # Sentry Auth Token | ||||
| .sentryclirc | ||||
							
								
								
									
										2
									
								
								app/.prettierignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| *.schema.json | ||||
| pnpm-lock.yaml | ||||
							
								
								
									
										3
									
								
								app/.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| { | ||||
|   "eslint.format.enable": true | ||||
| } | ||||
| @@ -12,11 +12,22 @@ declare module "nextjs-routes" { | ||||
| 
 | ||||
|   export type Route = | ||||
|     | StaticRoute<"/account/signin"> | ||||
|     | DynamicRoute<"/api/[...trpc]", { "trpc": string[] }> | ||||
|     | DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }> | ||||
|     | StaticRoute<"/api/experiments/og-image"> | ||||
|     | StaticRoute<"/api/openapi"> | ||||
|     | StaticRoute<"/api/sentry-example-api"> | ||||
|     | DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }> | ||||
|     | DynamicRoute<"/data/[id]", { "id": string }> | ||||
|     | StaticRoute<"/data"> | ||||
|     | DynamicRoute<"/experiments/[id]", { "id": string }> | ||||
|     | StaticRoute<"/experiments"> | ||||
|     | StaticRoute<"/">; | ||||
|     | StaticRoute<"/"> | ||||
|     | StaticRoute<"/logged-calls"> | ||||
|     | StaticRoute<"/project/settings"> | ||||
|     | StaticRoute<"/sentry-example-page"> | ||||
|     | StaticRoute<"/world-champs"> | ||||
|     | StaticRoute<"/world-champs/signup">; | ||||
| 
 | ||||
|   interface StaticRoute<Pathname> { | ||||
|     pathname: Pathname; | ||||
| @@ -20,6 +20,10 @@ FROM base as builder | ||||
| # Include all NEXT_PUBLIC_* env vars here | ||||
| ARG NEXT_PUBLIC_POSTHOG_KEY | ||||
| 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 | ||||
| COPY --from=deps /app/node_modules ./node_modules | ||||
							
								
								
									
										61
									
								
								app/next.config.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,61 @@ | ||||
| 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 | ||||
|  * for Docker builds. | ||||
|  */ | ||||
| const { env } = await import("./src/env.mjs"); | ||||
|  | ||||
| /** @type {import("next").NextConfig} */ | ||||
| let config = { | ||||
|   reactStrictMode: true, | ||||
|  | ||||
|   /** | ||||
|    * If you have `experimental: { appDir: true }` set, then you must comment the below `i18n` config | ||||
|    * out. | ||||
|    * | ||||
|    * @see https://github.com/vercel/next.js/issues/41980 | ||||
|    */ | ||||
|   i18n: { | ||||
|     locales: ["en"], | ||||
|     defaultLocale: "en", | ||||
|   }, | ||||
|  | ||||
|   rewrites: async () => [ | ||||
|     { | ||||
|       source: "/ingest/:path*", | ||||
|       destination: "https://app.posthog.com/:path*", | ||||
|     }, | ||||
|   ], | ||||
|  | ||||
|   webpack: (config) => { | ||||
|     config.module.rules.push({ | ||||
|       test: /\.txt$/, | ||||
|       use: "raw-loader", | ||||
|     }); | ||||
|     return 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; | ||||
							
								
								
									
										7
									
								
								app/openapitools.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | ||||
| { | ||||
|   "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", | ||||
|   "spaces": 2, | ||||
|   "generator-cli": { | ||||
|     "version": "6.6.0" | ||||
|   } | ||||
| } | ||||
| @@ -12,20 +12,24 @@ | ||||
|     "dev:next": "next dev", | ||||
|     "dev:wss": "pnpm tsx --watch src/wss-server.ts", | ||||
|     "dev:worker": "NODE_ENV='development' pnpm tsx --watch src/server/tasks/worker.ts", | ||||
|     "dev": "concurrently --kill-others 'pnpm dev:next' 'pnpm dev:wss'", | ||||
|     "dev": "concurrently --kill-others 'pnpm dev:next' 'pnpm dev:wss' 'pnpm dev:worker'", | ||||
|     "postinstall": "prisma generate", | ||||
|     "lint": "next lint", | ||||
|     "start": "next start", | ||||
|     "codegen": "tsx src/codegen/export-openai-types.ts", | ||||
|     "codegen": "tsx src/server/scripts/client-codegen.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": { | ||||
|     "@anthropic-ai/sdk": "^0.5.8", | ||||
|     "@apidevtools/json-schema-ref-parser": "^10.1.0", | ||||
|     "@babel/preset-typescript": "^7.22.5", | ||||
|     "@babel/standalone": "^7.22.9", | ||||
|     "@chakra-ui/anatomy": "^2.2.0", | ||||
|     "@chakra-ui/next-js": "^2.1.4", | ||||
|     "@chakra-ui/react": "^2.7.1", | ||||
|     "@chakra-ui/styled-system": "^2.9.1", | ||||
|     "@emotion/react": "^11.11.1", | ||||
|     "@emotion/server": "^11.11.0", | ||||
|     "@emotion/styled": "^11.11.0", | ||||
| @@ -33,6 +37,7 @@ | ||||
|     "@monaco-editor/loader": "^1.3.3", | ||||
|     "@next-auth/prisma-adapter": "^1.0.5", | ||||
|     "@prisma/client": "^4.14.0", | ||||
|     "@sentry/nextjs": "^7.61.0", | ||||
|     "@t3-oss/env-nextjs": "^0.3.1", | ||||
|     "@tabler/icons-react": "^2.22.0", | ||||
|     "@tanstack/react-query": "^4.29.7", | ||||
| @@ -40,10 +45,12 @@ | ||||
|     "@trpc/next": "^10.26.0", | ||||
|     "@trpc/react-query": "^10.26.0", | ||||
|     "@trpc/server": "^10.26.0", | ||||
|     "@vercel/og": "^0.5.9", | ||||
|     "ast-types": "^0.14.2", | ||||
|     "chroma-js": "^2.4.2", | ||||
|     "concurrently": "^8.2.0", | ||||
|     "cors": "^2.8.5", | ||||
|     "crypto-random-string": "^5.0.0", | ||||
|     "dayjs": "^1.11.8", | ||||
|     "dedent": "^1.0.1", | ||||
|     "dotenv": "^16.3.1", | ||||
| @@ -56,29 +63,41 @@ | ||||
|     "json-schema-to-typescript": "^13.0.2", | ||||
|     "json-stringify-pretty-compact": "^4.0.0", | ||||
|     "jsonschema": "^1.4.1", | ||||
|     "kysely": "^0.26.1", | ||||
|     "lodash-es": "^4.17.21", | ||||
|     "lucide-react": "^0.265.0", | ||||
|     "next": "^13.4.2", | ||||
|     "next-auth": "^4.22.1", | ||||
|     "next-query-params": "^4.2.3", | ||||
|     "nextjs-cors": "^2.1.2", | ||||
|     "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", | ||||
|     "posthog-js": "^1.68.4", | ||||
|     "posthog-js": "^1.75.3", | ||||
|     "posthog-node": "^3.1.1", | ||||
|     "prettier": "^3.0.0", | ||||
|     "prismjs": "^1.29.0", | ||||
|     "react": "18.2.0", | ||||
|     "react-diff-viewer": "^3.1.1", | ||||
|     "react-dom": "18.2.0", | ||||
|     "react-github-btn": "^1.4.0", | ||||
|     "react-icons": "^4.10.1", | ||||
|     "react-json-tree": "^0.18.0", | ||||
|     "react-select": "^5.7.4", | ||||
|     "react-syntax-highlighter": "^15.5.0", | ||||
|     "react-textarea-autosize": "^8.5.0", | ||||
|     "recast": "^0.23.3", | ||||
|     "recharts": "^2.7.2", | ||||
|     "replicate": "^0.12.3", | ||||
|     "socket.io": "^4.7.1", | ||||
|     "socket.io-client": "^4.7.1", | ||||
|     "superjson": "1.12.2", | ||||
|     "trpc-openapi": "^1.2.0", | ||||
|     "tsx": "^3.12.7", | ||||
|     "type-fest": "^4.0.0", | ||||
|     "use-query-params": "^2.2.1", | ||||
|     "uuid": "^9.0.0", | ||||
|     "vite-tsconfig-paths": "^4.2.0", | ||||
|     "zod": "^3.21.4", | ||||
|     "zustand": "^4.3.9" | ||||
| @@ -94,13 +113,16 @@ | ||||
|     "@types/json-schema": "^7.0.12", | ||||
|     "@types/lodash-es": "^4.17.8", | ||||
|     "@types/node": "^18.16.0", | ||||
|     "@types/pg": "^8.10.2", | ||||
|     "@types/pluralize": "^0.0.30", | ||||
|     "@types/prismjs": "^1.26.0", | ||||
|     "@types/react": "^18.2.6", | ||||
|     "@types/react-dom": "^18.2.4", | ||||
|     "@types/react-syntax-highlighter": "^15.5.7", | ||||
|     "@types/uuid": "^9.0.2", | ||||
|     "@typescript-eslint/eslint-plugin": "^5.59.6", | ||||
|     "@typescript-eslint/parser": "^5.59.6", | ||||
|     "csv-parse": "^5.4.0", | ||||
|     "eslint": "^8.40.0", | ||||
|     "eslint-config-next": "^13.4.2", | ||||
|     "eslint-plugin-unused-imports": "^2.0.0", | ||||
							
								
								
									
										1679
									
								
								pnpm-lock.yaml → app/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										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 | ||||
| 
 | 
| @@ -0,0 +1,8 @@ | ||||
| /* | ||||
|   Warnings: | ||||
|  | ||||
|   - You are about to drop the column `streamingChannel` on the `ScenarioVariantCell` table. All the data in the column will be lost. | ||||
|  | ||||
| */ | ||||
| -- AlterTable | ||||
| ALTER TABLE "ScenarioVariantCell" DROP COLUMN "streamingChannel"; | ||||
| @@ -0,0 +1,52 @@ | ||||
| -- DropForeignKey | ||||
| ALTER TABLE "ModelOutput" DROP CONSTRAINT "ModelOutput_scenarioVariantCellId_fkey"; | ||||
|  | ||||
| -- DropForeignKey | ||||
| ALTER TABLE "OutputEvaluation" DROP CONSTRAINT "OutputEvaluation_modelOutputId_fkey"; | ||||
|  | ||||
| -- DropIndex | ||||
| DROP INDEX "OutputEvaluation_modelOutputId_evaluationId_key"; | ||||
|  | ||||
| -- AlterTable | ||||
| ALTER TABLE "OutputEvaluation" RENAME COLUMN "modelOutputId" TO "modelResponseId"; | ||||
|  | ||||
| -- AlterTable | ||||
| ALTER TABLE "ScenarioVariantCell" DROP COLUMN "retryTime", | ||||
| DROP COLUMN "statusCode", | ||||
| ADD COLUMN     "jobQueuedAt" TIMESTAMP(3), | ||||
| ADD COLUMN     "jobStartedAt" TIMESTAMP(3); | ||||
|  | ||||
| ALTER TABLE "ModelOutput" RENAME TO "ModelResponse"; | ||||
|  | ||||
| ALTER TABLE "ModelResponse"  | ||||
| ADD COLUMN "requestedAt" TIMESTAMP(3), | ||||
| ADD COLUMN "receivedAt" TIMESTAMP(3), | ||||
| ADD COLUMN "statusCode" INTEGER, | ||||
| ADD COLUMN "errorMessage" TEXT, | ||||
| ADD COLUMN "retryTime" TIMESTAMP(3), | ||||
| ADD COLUMN "outdated" BOOLEAN NOT NULL DEFAULT false; | ||||
|  | ||||
| -- 3. Remove the unnecessary column | ||||
| ALTER TABLE "ModelResponse" | ||||
| DROP COLUMN "timeToComplete"; | ||||
|  | ||||
| -- AlterTable | ||||
| ALTER TABLE "ModelResponse" RENAME CONSTRAINT "ModelOutput_pkey" TO "ModelResponse_pkey"; | ||||
| ALTER TABLE "ModelResponse" ALTER COLUMN "output" DROP NOT NULL; | ||||
|  | ||||
| -- DropIndex | ||||
| DROP INDEX "ModelOutput_scenarioVariantCellId_key"; | ||||
|  | ||||
| -- AddForeignKey | ||||
| ALTER TABLE "ModelResponse" ADD CONSTRAINT "ModelResponse_scenarioVariantCellId_fkey" FOREIGN KEY ("scenarioVariantCellId") REFERENCES "ScenarioVariantCell"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||||
|  | ||||
| -- RenameIndex | ||||
| ALTER INDEX "ModelOutput_inputHash_idx" RENAME TO "ModelResponse_inputHash_idx"; | ||||
|  | ||||
| -- CreateIndex | ||||
| CREATE UNIQUE INDEX "OutputEvaluation_modelResponseId_evaluationId_key" ON "OutputEvaluation"("modelResponseId", "evaluationId"); | ||||
|  | ||||
| -- AddForeignKey | ||||
| ALTER TABLE "OutputEvaluation" ADD CONSTRAINT "OutputEvaluation_modelResponseId_fkey" FOREIGN KEY ("modelResponseId") REFERENCES "ModelResponse"("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; | ||||
| @@ -0,0 +1,37 @@ | ||||
| -- Rename Enum | ||||
| ALTER TYPE "OrganizationUserRole" RENAME TO "ProjectUserRole"; | ||||
|  | ||||
| -- Drop and recreate foreign keys | ||||
| ALTER TABLE "ApiKey" DROP CONSTRAINT "ApiKey_organizationId_fkey"; | ||||
| ALTER TABLE "Dataset" DROP CONSTRAINT "Dataset_organizationId_fkey"; | ||||
| ALTER TABLE "Experiment" DROP CONSTRAINT "Experiment_organizationId_fkey"; | ||||
| ALTER TABLE "LoggedCall" DROP CONSTRAINT "LoggedCall_organizationId_fkey"; | ||||
| ALTER TABLE "OrganizationUser" DROP CONSTRAINT "OrganizationUser_organizationId_fkey"; | ||||
| ALTER TABLE "OrganizationUser" DROP CONSTRAINT "OrganizationUser_userId_fkey"; | ||||
|  | ||||
| -- Rename columns | ||||
| ALTER TABLE "ApiKey" RENAME COLUMN "organizationId" TO "projectId"; | ||||
| ALTER TABLE "Dataset" RENAME COLUMN "organizationId" TO "projectId"; | ||||
| ALTER TABLE "Experiment" RENAME COLUMN "organizationId" TO "projectId"; | ||||
| ALTER TABLE "LoggedCall" RENAME COLUMN "organizationId" TO "projectId"; | ||||
| ALTER TABLE "OrganizationUser" RENAME COLUMN "organizationId" TO "projectId"; | ||||
| ALTER TABLE "Organization" RENAME COLUMN "personalOrgUserId" TO "personalProjectUserId"; | ||||
|  | ||||
| -- Rename table | ||||
| ALTER TABLE "Organization" RENAME TO "Project"; | ||||
| ALTER TABLE "OrganizationUser" RENAME TO "ProjectUser"; | ||||
|  | ||||
| -- Recreate foreign keys | ||||
| ALTER TABLE "Experiment" ADD CONSTRAINT "Experiment_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||||
| ALTER TABLE "Dataset" ADD CONSTRAINT "Dataset_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||||
| ALTER TABLE "ProjectUser" ADD CONSTRAINT "ProjectUser_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||||
| ALTER TABLE "ProjectUser" ADD CONSTRAINT "ProjectUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||||
| ALTER TABLE "LoggedCall" ADD CONSTRAINT "LoggedCall_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||||
| ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||||
|  | ||||
| -- Rename indexes | ||||
| ALTER TABLE "Project" RENAME CONSTRAINT "Organization_pkey" TO "Project_pkey"; | ||||
| ALTER TABLE "ProjectUser" RENAME CONSTRAINT "OrganizationUser_pkey" TO "ProjectUser_pkey"; | ||||
| ALTER TABLE "Project" RENAME CONSTRAINT "Organization_personalOrgUserId_fkey" TO "Project_personalProjectUserId_fkey"; | ||||
| ALTER INDEX "Organization_personalOrgUserId_key" RENAME TO "Project_personalProjectUserId_key"; | ||||
| ALTER INDEX "OrganizationUser_organizationId_userId_key" RENAME TO "ProjectUser_projectId_userId_key"; | ||||
							
								
								
									
										406
									
								
								app/prisma/schema.prisma
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,406 @@ | ||||
| // This is your Prisma schema file, | ||||
| // learn more about it in the docs: https://pris.ly/d/prisma-schema | ||||
|  | ||||
| generator client { | ||||
|     provider = "prisma-client-js" | ||||
| } | ||||
|  | ||||
| datasource db { | ||||
|     provider = "postgresql" | ||||
|     url      = env("DATABASE_URL") | ||||
| } | ||||
|  | ||||
| model Experiment { | ||||
|     id    String @id @default(uuid()) @db.Uuid | ||||
|     label String | ||||
|  | ||||
|     sortIndex Int @default(0) | ||||
|  | ||||
|     projectId String   @db.Uuid | ||||
|     project   Project? @relation(fields: [projectId], references: [id], onDelete: Cascade) | ||||
|  | ||||
|     createdAt DateTime @default(now()) | ||||
|     updatedAt DateTime @updatedAt | ||||
|  | ||||
|     templateVariables TemplateVariable[] | ||||
|     promptVariants    PromptVariant[] | ||||
|     testScenarios     TestScenario[] | ||||
|     evaluations       Evaluation[] | ||||
| } | ||||
|  | ||||
| model PromptVariant { | ||||
|     id String @id @default(uuid()) @db.Uuid | ||||
|  | ||||
|     label                    String | ||||
|     promptConstructor        String | ||||
|     promptConstructorVersion Int | ||||
|     model                    String | ||||
|     modelProvider            String | ||||
|  | ||||
|     uiId      String  @default(uuid()) @db.Uuid | ||||
|     visible   Boolean @default(true) | ||||
|     sortIndex Int     @default(0) | ||||
|  | ||||
|     experimentId String     @db.Uuid | ||||
|     experiment   Experiment @relation(fields: [experimentId], references: [id], onDelete: Cascade) | ||||
|  | ||||
|     createdAt            DateTime              @default(now()) | ||||
|     updatedAt            DateTime              @updatedAt | ||||
|     scenarioVariantCells ScenarioVariantCell[] | ||||
|  | ||||
|     @@index([uiId]) | ||||
| } | ||||
|  | ||||
| model TestScenario { | ||||
|     id String @id @default(uuid()) @db.Uuid | ||||
|  | ||||
|     variableValues Json | ||||
|  | ||||
|     uiId      String  @default(uuid()) @db.Uuid | ||||
|     visible   Boolean @default(true) | ||||
|     sortIndex Int     @default(0) | ||||
|  | ||||
|     experimentId String     @db.Uuid | ||||
|     experiment   Experiment @relation(fields: [experimentId], references: [id], onDelete: Cascade) | ||||
|  | ||||
|     createdAt            DateTime              @default(now()) | ||||
|     updatedAt            DateTime              @updatedAt | ||||
|     scenarioVariantCells ScenarioVariantCell[] | ||||
| } | ||||
|  | ||||
| model TemplateVariable { | ||||
|     id String @id @default(uuid()) @db.Uuid | ||||
|  | ||||
|     label String | ||||
|  | ||||
|     experimentId String     @db.Uuid | ||||
|     experiment   Experiment @relation(fields: [experimentId], references: [id], onDelete: Cascade) | ||||
|  | ||||
|     createdAt DateTime @default(now()) | ||||
|     updatedAt DateTime @updatedAt | ||||
| } | ||||
|  | ||||
| enum CellRetrievalStatus { | ||||
|     PENDING | ||||
|     IN_PROGRESS | ||||
|     COMPLETE | ||||
|     ERROR | ||||
| } | ||||
|  | ||||
| model ScenarioVariantCell { | ||||
|     id String @id @default(uuid()) @db.Uuid | ||||
|  | ||||
|     retrievalStatus CellRetrievalStatus @default(COMPLETE) | ||||
|     jobQueuedAt     DateTime? | ||||
|     jobStartedAt    DateTime? | ||||
|     modelResponses  ModelResponse[] | ||||
|     errorMessage    String? // Contains errors that occurred independently of model responses | ||||
|  | ||||
|     promptVariantId String        @db.Uuid | ||||
|     promptVariant   PromptVariant @relation(fields: [promptVariantId], references: [id], onDelete: Cascade) | ||||
|     prompt          Json? | ||||
|  | ||||
|     testScenarioId String       @db.Uuid | ||||
|     testScenario   TestScenario @relation(fields: [testScenarioId], references: [id], onDelete: Cascade) | ||||
|  | ||||
|     createdAt DateTime @default(now()) | ||||
|     updatedAt DateTime @updatedAt | ||||
|  | ||||
|     @@unique([promptVariantId, testScenarioId]) | ||||
| } | ||||
|  | ||||
| model ModelResponse { | ||||
|     id String @id @default(uuid()) @db.Uuid | ||||
|  | ||||
|     inputHash        String | ||||
|     requestedAt      DateTime? | ||||
|     receivedAt       DateTime? | ||||
|     output           Json? | ||||
|     cost             Float? | ||||
|     promptTokens     Int? | ||||
|     completionTokens Int? | ||||
|     statusCode       Int? | ||||
|     errorMessage     String? | ||||
|     retryTime        DateTime? | ||||
|     outdated         Boolean   @default(false) | ||||
|  | ||||
|     createdAt DateTime @default(now()) | ||||
|     updatedAt DateTime @updatedAt | ||||
|  | ||||
|     scenarioVariantCellId String              @db.Uuid | ||||
|     scenarioVariantCell   ScenarioVariantCell @relation(fields: [scenarioVariantCellId], references: [id], onDelete: Cascade) | ||||
|     outputEvaluations     OutputEvaluation[] | ||||
|  | ||||
|     @@index([inputHash]) | ||||
| } | ||||
|  | ||||
| enum EvalType { | ||||
|     CONTAINS | ||||
|     DOES_NOT_CONTAIN | ||||
|     GPT4_EVAL | ||||
| } | ||||
|  | ||||
| model Evaluation { | ||||
|     id String @id @default(uuid()) @db.Uuid | ||||
|  | ||||
|     label    String | ||||
|     evalType EvalType | ||||
|     value    String | ||||
|  | ||||
|     experimentId String     @db.Uuid | ||||
|     experiment   Experiment @relation(fields: [experimentId], references: [id], onDelete: Cascade) | ||||
|  | ||||
|     createdAt         DateTime           @default(now()) | ||||
|     updatedAt         DateTime           @updatedAt | ||||
|     outputEvaluations OutputEvaluation[] | ||||
| } | ||||
|  | ||||
| model OutputEvaluation { | ||||
|     id String @id @default(uuid()) @db.Uuid | ||||
|  | ||||
|     // Number between 0 (fail) and 1 (pass) | ||||
|     result  Float | ||||
|     details String? | ||||
|  | ||||
|     modelResponseId String        @db.Uuid | ||||
|     modelResponse   ModelResponse @relation(fields: [modelResponseId], references: [id], onDelete: Cascade) | ||||
|  | ||||
|     evaluationId String     @db.Uuid | ||||
|     evaluation   Evaluation @relation(fields: [evaluationId], references: [id], onDelete: Cascade) | ||||
|  | ||||
|     createdAt DateTime @default(now()) | ||||
|     updatedAt DateTime @updatedAt | ||||
|  | ||||
|     @@unique([modelResponseId, evaluationId]) | ||||
| } | ||||
|  | ||||
| model Dataset { | ||||
|     id String @id @default(uuid()) @db.Uuid | ||||
|  | ||||
|     name           String | ||||
|     datasetEntries DatasetEntry[] | ||||
|  | ||||
|     projectId String  @db.Uuid | ||||
|     project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade) | ||||
|  | ||||
|     createdAt DateTime @default(now()) | ||||
|     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 | ||||
| } | ||||
|  | ||||
| model Project { | ||||
|     id   String @id @default(uuid()) @db.Uuid | ||||
|     name String @default("Project 1") | ||||
|  | ||||
|     personalProjectUserId String? @unique @db.Uuid | ||||
|     personalProjectUser   User?   @relation(fields: [personalProjectUserId], references: [id], onDelete: Cascade) | ||||
|  | ||||
|     createdAt    DateTime      @default(now()) | ||||
|     updatedAt    DateTime      @updatedAt | ||||
|     projectUsers ProjectUser[] | ||||
|     experiments  Experiment[] | ||||
|     datasets     Dataset[] | ||||
|     loggedCalls  LoggedCall[] | ||||
|     apiKeys      ApiKey[] | ||||
| } | ||||
|  | ||||
| enum ProjectUserRole { | ||||
|     ADMIN | ||||
|     MEMBER | ||||
|     VIEWER | ||||
| } | ||||
|  | ||||
| model ProjectUser { | ||||
|     id String @id @default(uuid()) @db.Uuid | ||||
|  | ||||
|     role ProjectUserRole | ||||
|  | ||||
|     projectId String   @db.Uuid | ||||
|     project   Project? @relation(fields: [projectId], references: [id], onDelete: Cascade) | ||||
|  | ||||
|     userId String @db.Uuid | ||||
|     user   User   @relation(fields: [userId], references: [id], onDelete: Cascade) | ||||
|  | ||||
|     createdAt DateTime @default(now()) | ||||
|     updatedAt DateTime @updatedAt | ||||
|  | ||||
|     @@unique([projectId, 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") | ||||
|  | ||||
|     projectId String   @db.Uuid | ||||
|     project   Project? @relation(fields: [projectId], 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 | ||||
|  | ||||
|     projectId String   @db.Uuid | ||||
|     project   Project? @relation(fields: [projectId], references: [id], onDelete: Cascade) | ||||
|  | ||||
|     createdAt DateTime @default(now()) | ||||
|     updatedAt DateTime @updatedAt | ||||
| } | ||||
|  | ||||
| model Account { | ||||
|     id                       String  @id @default(uuid()) @db.Uuid | ||||
|     userId                   String  @db.Uuid | ||||
|     type                     String | ||||
|     provider                 String | ||||
|     providerAccountId        String | ||||
|     refresh_token            String? @db.Text | ||||
|     refresh_token_expires_in Int? | ||||
|     access_token             String? @db.Text | ||||
|     expires_at               Int? | ||||
|     token_type               String? | ||||
|     scope                    String? | ||||
|     id_token                 String? @db.Text | ||||
|     session_state            String? | ||||
|     user                     User    @relation(fields: [userId], references: [id], onDelete: Cascade) | ||||
|  | ||||
|     @@unique([provider, providerAccountId]) | ||||
| } | ||||
|  | ||||
| model Session { | ||||
|     id           String   @id @default(uuid()) @db.Uuid | ||||
|     sessionToken String   @unique | ||||
|     userId       String   @db.Uuid | ||||
|     expires      DateTime | ||||
|     user         User     @relation(fields: [userId], references: [id], onDelete: Cascade) | ||||
| } | ||||
|  | ||||
| enum UserRole { | ||||
|     ADMIN | ||||
|     USER | ||||
| } | ||||
|  | ||||
| model User { | ||||
|     id            String    @id @default(uuid()) @db.Uuid | ||||
|     name          String? | ||||
|     email         String?   @unique | ||||
|     emailVerified DateTime? | ||||
|     image         String? | ||||
|  | ||||
|     role UserRole @default(USER) | ||||
|  | ||||
|     accounts          Account[] | ||||
|     sessions          Session[] | ||||
|     projectUsers      ProjectUser[] | ||||
|     projects          Project[] | ||||
|     worldChampEntrant WorldChampEntrant? | ||||
|  | ||||
|     createdAt DateTime @default(now()) | ||||
|     updatedAt DateTime @default(now()) @updatedAt | ||||
| } | ||||
|  | ||||
| model VerificationToken { | ||||
|     identifier String | ||||
|     token      String   @unique | ||||
|     expires    DateTime | ||||
|  | ||||
|     @@unique([identifier, token]) | ||||
| } | ||||
| @@ -1,15 +1,20 @@ | ||||
| import { prisma } from "~/server/db"; | ||||
| import dedent from "dedent"; | ||||
| import { generateNewCell } from "~/server/utils/generateNewCell"; | ||||
| import { promptConstructorVersion } from "~/promptConstructor/version"; | ||||
| 
 | ||||
| const defaultId = "11111111-1111-1111-1111-111111111111"; | ||||
| 
 | ||||
| await prisma.organization.deleteMany({ | ||||
| await prisma.project.deleteMany({ | ||||
|   where: { id: defaultId }, | ||||
| }); | ||||
| await prisma.organization.create({ | ||||
|   data: { id: defaultId }, | ||||
| }); | ||||
| 
 | ||||
| // If there's an existing project, just seed into it
 | ||||
| const project = | ||||
|   (await prisma.project.findFirst({})) ?? | ||||
|   (await prisma.project.create({ | ||||
|     data: { id: defaultId }, | ||||
|   })); | ||||
| 
 | ||||
| await prisma.experiment.deleteMany({ | ||||
|   where: { | ||||
| @@ -21,7 +26,7 @@ await prisma.experiment.create({ | ||||
|   data: { | ||||
|     id: defaultId, | ||||
|     label: "Country Capitals Example", | ||||
|     organizationId: defaultId, | ||||
|     projectId: project.id, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| @@ -47,8 +52,8 @@ await prisma.promptVariant.createMany({ | ||||
|       sortIndex: 0, | ||||
|       model: "gpt-3.5-turbo-0613", | ||||
|       modelProvider: "openai/ChatCompletion", | ||||
|       constructFnVersion: 1, | ||||
|       constructFn: dedent` | ||||
|       promptConstructorVersion, | ||||
|       promptConstructor: dedent` | ||||
|         definePrompt("openai/ChatCompletion", { | ||||
|           model: "gpt-3.5-turbo-0613", | ||||
|           messages: [ | ||||
| @@ -66,8 +71,8 @@ await prisma.promptVariant.createMany({ | ||||
|       sortIndex: 1, | ||||
|       model: "gpt-3.5-turbo-0613", | ||||
|       modelProvider: "openai/ChatCompletion", | ||||
|       constructFnVersion: 1, | ||||
|       constructFn: dedent` | ||||
|       promptConstructorVersion, | ||||
|       promptConstructor: dedent` | ||||
|         definePrompt("openai/ChatCompletion", { | ||||
|           model: "gpt-3.5-turbo-0613", | ||||
|           messages: [ | ||||
| @@ -103,30 +108,41 @@ await prisma.testScenario.deleteMany({ | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| const countries = [ | ||||
|   "Afghanistan", | ||||
|   "Albania", | ||||
|   "Algeria", | ||||
|   "Andorra", | ||||
|   "Angola", | ||||
|   "Antigua and Barbuda", | ||||
|   "Argentina", | ||||
|   "Armenia", | ||||
|   "Australia", | ||||
|   "Austria", | ||||
|   "Austrian Empire", | ||||
|   "Azerbaijan", | ||||
|   "Baden", | ||||
|   "Bahamas, The", | ||||
|   "Bahrain", | ||||
|   "Bangladesh", | ||||
|   "Barbados", | ||||
|   "Bavaria", | ||||
|   "Belarus", | ||||
|   "Belgium", | ||||
|   "Belize", | ||||
|   "Benin (Dahomey)", | ||||
|   "Bolivia", | ||||
|   "Bosnia and Herzegovina", | ||||
|   "Botswana", | ||||
| ]; | ||||
| await prisma.testScenario.createMany({ | ||||
|   data: [ | ||||
|     { | ||||
|       experimentId: defaultId, | ||||
|       sortIndex: 0, | ||||
|       variableValues: { | ||||
|         country: "Spain", | ||||
|       }, | ||||
|   data: countries.map((country, i) => ({ | ||||
|     experimentId: defaultId, | ||||
|     sortIndex: i, | ||||
|     variableValues: { | ||||
|       country: country, | ||||
|     }, | ||||
|     { | ||||
|       experimentId: defaultId, | ||||
|       sortIndex: 1, | ||||
|       variableValues: { | ||||
|         country: "USA", | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       experimentId: defaultId, | ||||
|       sortIndex: 2, | ||||
|       variableValues: { | ||||
|         country: "Chile", | ||||
|       }, | ||||
|     }, | ||||
|   ], | ||||
|   })), | ||||
| }); | ||||
| 
 | ||||
| const variants = await prisma.promptVariant.findMany({ | ||||
| @@ -149,5 +165,5 @@ await Promise.all( | ||||
|         testScenarioId: scenario.id, | ||||
|       })), | ||||
|     ) | ||||
|     .map((cell) => generateNewCell(cell.promptVariantId, cell.testScenarioId)), | ||||
|     .map((cell) => generateNewCell(cell.promptVariantId, cell.testScenarioId, { stream: false })), | ||||
| ); | ||||
							
								
								
									
										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.project.deleteMany({ | ||||
|   where: { id: defaultId }, | ||||
| }); | ||||
|  | ||||
| // If there's an existing project, just seed into it | ||||
| const project = | ||||
|   (await prisma.project.findFirst({})) ?? | ||||
|   (await prisma.project.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, | ||||
|       projectId: project.id, | ||||
|     }, | ||||
|   }); | ||||
|   if (oldExperiment) { | ||||
|     await prisma.experiment.deleteMany({ | ||||
|       where: { id: oldExperiment.id }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   const experiment = await prisma.experiment.create({ | ||||
|     data: { | ||||
|       id: oldExperiment?.id ?? undefined, | ||||
|       label: experimentName, | ||||
|       projectId: project.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
									
								
							
							
						
						
							
								
								
									
										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.project.deleteMany({ | ||||
|   where: { id: defaultId }, | ||||
| }); | ||||
|  | ||||
| // If there's an existing project, just seed into it | ||||
| const project = | ||||
|   (await prisma.project.findFirst({})) ?? | ||||
|   (await prisma.project.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, | ||||
|     projectId: project.id, | ||||
|   }, | ||||
| }); | ||||
| if (oldExperiment) { | ||||
|   await prisma.experiment.deleteMany({ | ||||
|     where: { id: oldExperiment.id }, | ||||
|   }); | ||||
| } | ||||
|  | ||||
| const experiment = await prisma.experiment.create({ | ||||
|   data: { | ||||
|     id: oldExperiment?.id ?? undefined, | ||||
|     label: experimentName, | ||||
|     projectId: project.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}}", | ||||
|     }, | ||||
|   ], | ||||
| }); | ||||
| Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB | 
| Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB | 
| Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB | 
| Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB | 
| Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 704 B | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
| Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB | 
| Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB | 
| Before Width: | Height: | Size: 858 B After Width: | Height: | Size: 858 B | 
							
								
								
									
										
											BIN
										
									
								
								app/public/fonts/Inconsolata_SemiExpanded-Medium.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/public/og.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 62 KiB | 
							
								
								
									
										15
									
								
								app/run-prod.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | ||||
| #! /bin/bash | ||||
|  | ||||
| set -e | ||||
|  | ||||
| echo "Migrating the database" | ||||
| pnpm prisma migrate deploy | ||||
|  | ||||
| echo "Migrating promptConstructors" | ||||
| pnpm tsx src/promptConstructor/migrate.ts | ||||
|  | ||||
| echo "Starting the server" | ||||
|  | ||||
| pnpm concurrently --kill-others \ | ||||
|   "pnpm start" \ | ||||
|   "pnpm tsx src/server/tasks/worker.ts" | ||||
							
								
								
									
										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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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, | ||||
|   }); | ||||
| } | ||||
| @@ -1,5 +1,7 @@ | ||||
| import { | ||||
|   Button, | ||||
|   HStack, | ||||
|   Icon, | ||||
|   Modal, | ||||
|   ModalBody, | ||||
|   ModalCloseButton, | ||||
| @@ -7,33 +9,37 @@ import { | ||||
|   ModalFooter, | ||||
|   ModalHeader, | ||||
|   ModalOverlay, | ||||
|   VStack, | ||||
|   Text, | ||||
|   Spinner, | ||||
|   HStack, | ||||
|   Icon, | ||||
|   Text, | ||||
|   VStack, | ||||
| } from "@chakra-ui/react"; | ||||
| import { RiExchangeFundsFill } from "react-icons/ri"; | ||||
| import { useState } from "react"; | ||||
| import { type SupportedModel } from "~/server/types"; | ||||
| import { ModelStatsCard } from "./ModelStatsCard"; | ||||
| import { SelectModelSearch } from "./SelectModelSearch"; | ||||
| import { api } from "~/utils/api"; | ||||
| import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks"; | ||||
| import CompareFunctions from "../RefinePromptModal/CompareFunctions"; | ||||
| import { type PromptVariant } from "@prisma/client"; | ||||
| import { isObject, isString } from "lodash-es"; | ||||
| import { useState } from "react"; | ||||
| import { RiExchangeFundsFill } from "react-icons/ri"; | ||||
| import { type ProviderModel } from "~/modelProviders/types"; | ||||
| import { api } from "~/utils/api"; | ||||
| import { useExperiment, useHandledAsyncCallback, useVisibleScenarioIds } from "~/utils/hooks"; | ||||
| import { lookupModel, modelLabel } from "~/utils/utils"; | ||||
| import CompareFunctions from "../RefinePromptModal/CompareFunctions"; | ||||
| import { ModelSearch } from "./ModelSearch"; | ||||
| import { ModelStatsCard } from "./ModelStatsCard"; | ||||
| 
 | ||||
| export const SelectModelModal = ({ | ||||
| export const ChangeModelModal = ({ | ||||
|   variant, | ||||
|   onClose, | ||||
| }: { | ||||
|   variant: PromptVariant; | ||||
|   onClose: () => void; | ||||
| }) => { | ||||
|   const originalModel = variant.model as SupportedModel; | ||||
|   const [selectedModel, setSelectedModel] = useState<SupportedModel>(originalModel); | ||||
|   const [convertedModel, setConvertedModel] = useState<SupportedModel | undefined>(undefined); | ||||
|   const originalModel = lookupModel(variant.modelProvider, variant.model); | ||||
|   const [selectedModel, setSelectedModel] = useState({ | ||||
|     provider: variant.modelProvider, | ||||
|     model: variant.model, | ||||
|   } as ProviderModel); | ||||
|   const [convertedModel, setConvertedModel] = useState<ProviderModel | undefined>(); | ||||
|   const visibleScenarios = useVisibleScenarioIds(); | ||||
| 
 | ||||
|   const utils = api.useContext(); | ||||
| 
 | ||||
|   const experiment = useExperiment(); | ||||
| @@ -62,12 +68,18 @@ export const SelectModelModal = ({ | ||||
|       return; | ||||
|     await replaceVariantMutation.mutateAsync({ | ||||
|       id: variant.id, | ||||
|       constructFn: modifiedPromptFn, | ||||
|       promptConstructor: modifiedPromptFn, | ||||
|       streamScenarios: visibleScenarios, | ||||
|     }); | ||||
|     await utils.promptVariants.list.invalidate(); | ||||
|     onClose(); | ||||
|   }, [replaceVariantMutation, variant, onClose, modifiedPromptFn]); | ||||
| 
 | ||||
|   const originalLabel = modelLabel(variant.modelProvider, variant.model); | ||||
|   const selectedLabel = modelLabel(selectedModel.provider, selectedModel.model); | ||||
|   const convertedLabel = | ||||
|     convertedModel && modelLabel(convertedModel.provider, convertedModel.model); | ||||
| 
 | ||||
|   return ( | ||||
|     <Modal | ||||
|       isOpen | ||||
| @@ -86,16 +98,19 @@ export const SelectModelModal = ({ | ||||
|         <ModalBody maxW="unset"> | ||||
|           <VStack spacing={8}> | ||||
|             <ModelStatsCard label="Original Model" model={originalModel} /> | ||||
|             {originalModel !== selectedModel && ( | ||||
|               <ModelStatsCard label="New Model" model={selectedModel} /> | ||||
|             {originalLabel !== selectedLabel && ( | ||||
|               <ModelStatsCard | ||||
|                 label="New Model" | ||||
|                 model={lookupModel(selectedModel.provider, selectedModel.model)} | ||||
|               /> | ||||
|             )} | ||||
|             <SelectModelSearch selectedModel={selectedModel} setSelectedModel={setSelectedModel} /> | ||||
|             <ModelSearch selectedModel={selectedModel} setSelectedModel={setSelectedModel} /> | ||||
|             {isString(modifiedPromptFn) && ( | ||||
|               <CompareFunctions | ||||
|                 originalFunction={variant.constructFn} | ||||
|                 originalFunction={variant.promptConstructor} | ||||
|                 newFunction={modifiedPromptFn} | ||||
|                 leftTitle={originalModel} | ||||
|                 rightTitle={convertedModel} | ||||
|                 leftTitle={originalLabel} | ||||
|                 rightTitle={convertedLabel} | ||||
|               /> | ||||
|             )} | ||||
|           </VStack> | ||||
| @@ -107,7 +122,7 @@ export const SelectModelModal = ({ | ||||
|               colorScheme="gray" | ||||
|               onClick={getModifiedPromptFn} | ||||
|               minW={24} | ||||
|               isDisabled={originalModel === selectedModel || modificationInProgress} | ||||
|               isDisabled={originalLabel === selectedLabel || modificationInProgress} | ||||
|             > | ||||
|               {modificationInProgress ? <Spinner boxSize={4} /> : <Text>Convert</Text>} | ||||
|             </Button> | ||||
							
								
								
									
										36
									
								
								app/src/components/ChangeModelModal/ModelSearch.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,36 @@ | ||||
| import { Text, VStack } from "@chakra-ui/react"; | ||||
| import { type LegacyRef } from "react"; | ||||
| import Select from "react-select"; | ||||
| import { useElementDimensions } from "~/utils/hooks"; | ||||
|  | ||||
| import { flatMap } from "lodash-es"; | ||||
| import frontendModelProviders from "~/modelProviders/frontendModelProviders"; | ||||
| import { type ProviderModel } from "~/modelProviders/types"; | ||||
| import { modelLabel } from "~/utils/utils"; | ||||
|  | ||||
| const modelOptions = flatMap(Object.entries(frontendModelProviders), ([providerId, provider]) => | ||||
|   Object.entries(provider.models).map(([modelId]) => ({ | ||||
|     provider: providerId, | ||||
|     model: modelId, | ||||
|   })), | ||||
| ) as ProviderModel[]; | ||||
|  | ||||
| export const ModelSearch = (props: { | ||||
|   selectedModel: ProviderModel; | ||||
|   setSelectedModel: (model: ProviderModel) => void; | ||||
| }) => { | ||||
|   const [containerRef, containerDimensions] = useElementDimensions(); | ||||
|  | ||||
|   return ( | ||||
|     <VStack ref={containerRef as LegacyRef<HTMLDivElement>} w="full" fontFamily="inconsolata"> | ||||
|       <Text fontWeight="bold">Browse Models</Text> | ||||
|       <Select<ProviderModel> | ||||
|         styles={{ control: (provided) => ({ ...provided, width: containerDimensions?.width }) }} | ||||
|         getOptionLabel={(data) => modelLabel(data.provider, data.model)} | ||||
|         getOptionValue={(data) => modelLabel(data.provider, data.model)} | ||||
|         options={modelOptions} | ||||
|         onChange={(option) => option && props.setSelectedModel(option)} | ||||
|       /> | ||||
|     </VStack> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										117
									
								
								app/src/components/ChangeModelModal/ModelStatsCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,117 @@ | ||||
| import { | ||||
|   GridItem, | ||||
|   HStack, | ||||
|   Link, | ||||
|   SimpleGrid, | ||||
|   Text, | ||||
|   VStack, | ||||
|   type StackProps, | ||||
| } from "@chakra-ui/react"; | ||||
| import { type lookupModel } from "~/utils/utils"; | ||||
|  | ||||
| export const ModelStatsCard = ({ | ||||
|   label, | ||||
|   model, | ||||
| }: { | ||||
|   label: string; | ||||
|   model: ReturnType<typeof lookupModel>; | ||||
| }) => { | ||||
|   if (!model) return null; | ||||
|   return ( | ||||
|     <VStack w="full" align="start"> | ||||
|       <Text fontWeight="bold" fontSize="sm" textTransform="uppercase"> | ||||
|         {label} | ||||
|       </Text> | ||||
|  | ||||
|       <VStack | ||||
|         w="full" | ||||
|         spacing={6} | ||||
|         borderWidth={1} | ||||
|         borderColor="gray.300" | ||||
|         p={4} | ||||
|         borderRadius={8} | ||||
|         fontFamily="inconsolata" | ||||
|       > | ||||
|         <HStack w="full" align="flex-start"> | ||||
|           <VStack flex={1} fontSize="lg" alignItems="flex-start"> | ||||
|             <Text as="span" fontWeight="bold" color="gray.900"> | ||||
|               {model.name} | ||||
|             </Text> | ||||
|             <Text as="span" color="gray.600" fontSize="sm"> | ||||
|               Provider: {model.provider} | ||||
|             </Text> | ||||
|           </VStack> | ||||
|           <Link | ||||
|             href={model.learnMoreUrl} | ||||
|             isExternal | ||||
|             color="blue.500" | ||||
|             fontWeight="bold" | ||||
|             fontSize="sm" | ||||
|             ml={2} | ||||
|           > | ||||
|             Learn More | ||||
|           </Link> | ||||
|         </HStack> | ||||
|         <SimpleGrid | ||||
|           w="full" | ||||
|           justifyContent="space-between" | ||||
|           alignItems="flex-start" | ||||
|           fontSize="sm" | ||||
|           columns={{ base: 2, md: 4 }} | ||||
|         > | ||||
|           <SelectedModelLabeledInfo label="Context Window" info={model.contextWindow} /> | ||||
|           {model.promptTokenPrice && ( | ||||
|             <SelectedModelLabeledInfo | ||||
|               label="Input" | ||||
|               info={ | ||||
|                 <Text> | ||||
|                   ${(model.promptTokenPrice * 1000).toFixed(3)} | ||||
|                   <Text color="gray.500"> / 1K tokens</Text> | ||||
|                 </Text> | ||||
|               } | ||||
|             /> | ||||
|           )} | ||||
|           {model.completionTokenPrice && ( | ||||
|             <SelectedModelLabeledInfo | ||||
|               label="Output" | ||||
|               info={ | ||||
|                 <Text> | ||||
|                   ${(model.completionTokenPrice * 1000).toFixed(3)} | ||||
|                   <Text color="gray.500"> / 1K tokens</Text> | ||||
|                 </Text> | ||||
|               } | ||||
|             /> | ||||
|           )} | ||||
|           {model.pricePerSecond && ( | ||||
|             <SelectedModelLabeledInfo | ||||
|               label="Price" | ||||
|               info={ | ||||
|                 <Text> | ||||
|                   ${model.pricePerSecond.toFixed(3)} | ||||
|                   <Text color="gray.500"> / second</Text> | ||||
|                 </Text> | ||||
|               } | ||||
|             /> | ||||
|           )} | ||||
|           <SelectedModelLabeledInfo label="Speed" info={<Text>{model.speed}</Text>} /> | ||||
|         </SimpleGrid> | ||||
|       </VStack> | ||||
|     </VStack> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| const SelectedModelLabeledInfo = ({ | ||||
|   label, | ||||
|   info, | ||||
|   ...props | ||||
| }: { | ||||
|   label: string; | ||||
|   info: string | number | React.ReactElement; | ||||
| } & StackProps) => ( | ||||
|   <GridItem> | ||||
|     <VStack alignItems="flex-start" {...props}> | ||||
|       <Text fontWeight="bold">{label}</Text> | ||||
|       <Text>{info}</Text> | ||||
|     </VStack> | ||||
|   </GridItem> | ||||
| ); | ||||
							
								
								
									
										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 AutoResizeTextArea from "../AutoResizeTextArea"; | ||||
| import AutoResizeTextArea from "./AutoResizeTextArea"; | ||||
| 
 | ||||
| export const CustomInstructionsInput = ({ | ||||
|   instructions, | ||||
|   setInstructions, | ||||
|   loading, | ||||
|   onSubmit, | ||||
|   placeholder = "Send custom instructions", | ||||
|   ...props | ||||
| }: { | ||||
|   instructions: string; | ||||
|   setInstructions: (instructions: string) => void; | ||||
|   loading: boolean; | ||||
|   onSubmit: () => void; | ||||
| }) => { | ||||
|   placeholder?: string; | ||||
| } & InputGroupProps) => { | ||||
|   return ( | ||||
|     <InputGroup | ||||
|       size="md" | ||||
| @@ -22,6 +33,7 @@ export const CustomInstructionsInput = ({ | ||||
|       borderRadius={8} | ||||
|       alignItems="center" | ||||
|       colorScheme="orange" | ||||
|       {...props} | ||||
|     > | ||||
|       <AutoResizeTextArea | ||||
|         value={instructions} | ||||
| @@ -33,7 +45,7 @@ export const CustomInstructionsInput = ({ | ||||
|             onSubmit(); | ||||
|           } | ||||
|         }} | ||||
|         placeholder="Send custom instructions" | ||||
|         placeholder={placeholder} | ||||
|         py={4} | ||||
|         pl={4} | ||||
|         pr={12} | ||||
							
								
								
									
										74
									
								
								app/src/components/ExperimentSettingsDrawer/DeleteButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,74 @@ | ||||
| import { | ||||
|   Button, | ||||
|   Icon, | ||||
|   AlertDialog, | ||||
|   AlertDialogBody, | ||||
|   AlertDialogFooter, | ||||
|   AlertDialogHeader, | ||||
|   AlertDialogContent, | ||||
|   AlertDialogOverlay, | ||||
|   useDisclosure, | ||||
|   Text, | ||||
| } from "@chakra-ui/react"; | ||||
|  | ||||
| import { useRouter } from "next/router"; | ||||
| import { useRef } from "react"; | ||||
| import { BsTrash } from "react-icons/bs"; | ||||
| import { useAppStore } from "~/state/store"; | ||||
| import { api } from "~/utils/api"; | ||||
| import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks"; | ||||
|  | ||||
| export const DeleteButton = () => { | ||||
|   const experiment = useExperiment(); | ||||
|   const mutation = api.experiments.delete.useMutation(); | ||||
|   const utils = api.useContext(); | ||||
|   const router = useRouter(); | ||||
|  | ||||
|   const closeDrawer = useAppStore((s) => s.closeDrawer); | ||||
|  | ||||
|   const { isOpen, onOpen, onClose } = useDisclosure(); | ||||
|   const cancelRef = useRef<HTMLButtonElement>(null); | ||||
|  | ||||
|   const [onDeleteConfirm] = useHandledAsyncCallback(async () => { | ||||
|     if (!experiment.data?.id) return; | ||||
|     await mutation.mutateAsync({ id: experiment.data.id }); | ||||
|     await utils.experiments.list.invalidate(); | ||||
|     await router.push({ pathname: "/experiments" }); | ||||
|     closeDrawer(); | ||||
|  | ||||
|     onClose(); | ||||
|   }, [mutation, experiment.data?.id, router]); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Button size="sm" variant="ghost" colorScheme="red" fontWeight="normal" onClick={onOpen}> | ||||
|         <Icon as={BsTrash} boxSize={4} /> | ||||
|         <Text ml={2}>Delete Experiment</Text> | ||||
|       </Button> | ||||
|  | ||||
|       <AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}> | ||||
|         <AlertDialogOverlay> | ||||
|           <AlertDialogContent> | ||||
|             <AlertDialogHeader fontSize="lg" fontWeight="bold"> | ||||
|               Delete Experiment | ||||
|             </AlertDialogHeader> | ||||
|  | ||||
|             <AlertDialogBody> | ||||
|               If you delete this experiment all the associated prompts and scenarios will be deleted | ||||
|               as well. Are you sure? | ||||
|             </AlertDialogBody> | ||||
|  | ||||
|             <AlertDialogFooter> | ||||
|               <Button ref={cancelRef} onClick={onClose}> | ||||
|                 Cancel | ||||
|               </Button> | ||||
|               <Button colorScheme="red" onClick={onDeleteConfirm} ml={3}> | ||||
|                 Delete | ||||
|               </Button> | ||||
|             </AlertDialogFooter> | ||||
|           </AlertDialogContent> | ||||
|         </AlertDialogOverlay> | ||||
|       </AlertDialog> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| @@ -6,13 +6,14 @@ import { | ||||
|   DrawerHeader, | ||||
|   DrawerOverlay, | ||||
|   Heading, | ||||
|   Stack, | ||||
|   VStack, | ||||
| } from "@chakra-ui/react"; | ||||
| import EditScenarioVars from "./EditScenarioVars"; | ||||
| import EditEvaluations from "./EditEvaluations"; | ||||
| import EditScenarioVars from "../OutputsTable/EditScenarioVars"; | ||||
| import EditEvaluations from "../OutputsTable/EditEvaluations"; | ||||
| import { useAppStore } from "~/state/store"; | ||||
| import { DeleteButton } from "./DeleteButton"; | ||||
| 
 | ||||
| export default function SettingsDrawer() { | ||||
| export default function ExperimentSettingsDrawer() { | ||||
|   const isOpen = useAppStore((state) => state.drawerOpen); | ||||
|   const closeDrawer = useAppStore((state) => state.closeDrawer); | ||||
| 
 | ||||
| @@ -22,13 +23,16 @@ export default function SettingsDrawer() { | ||||
|       <DrawerContent> | ||||
|         <DrawerCloseButton /> | ||||
|         <DrawerHeader> | ||||
|           <Heading size="md">Settings</Heading> | ||||
|           <Heading size="md">Experiment Settings</Heading> | ||||
|         </DrawerHeader> | ||||
|         <DrawerBody> | ||||
|           <Stack spacing={6}> | ||||
|             <EditScenarioVars /> | ||||
|             <EditEvaluations /> | ||||
|           </Stack> | ||||
|         <DrawerBody h="full" pb={4}> | ||||
|           <VStack h="full" justifyContent="space-between"> | ||||
|             <VStack spacing={6}> | ||||
|               <EditScenarioVars /> | ||||
|               <EditEvaluations /> | ||||
|             </VStack> | ||||
|             <DeleteButton /> | ||||
|           </VStack> | ||||
|         </DrawerBody> | ||||
|       </DrawerContent> | ||||
|     </Drawer> | ||||
							
								
								
									
										57
									
								
								app/src/components/OutputsTable/AddVariantButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,57 @@ | ||||
| import { Box, Flex, Icon, Spinner } from "@chakra-ui/react"; | ||||
| import { BsPlus } from "react-icons/bs"; | ||||
| import { Text } from "@chakra-ui/react"; | ||||
| import { api } from "~/utils/api"; | ||||
| import { | ||||
|   useExperiment, | ||||
|   useExperimentAccess, | ||||
|   useHandledAsyncCallback, | ||||
|   useVisibleScenarioIds, | ||||
| } from "~/utils/hooks"; | ||||
| import { cellPadding } from "../constants"; | ||||
| import { ActionButton } from "./ScenariosHeader"; | ||||
|  | ||||
| export default function AddVariantButton() { | ||||
|   const experiment = useExperiment(); | ||||
|   const mutation = api.promptVariants.create.useMutation(); | ||||
|   const utils = api.useContext(); | ||||
|   const visibleScenarios = useVisibleScenarioIds(); | ||||
|  | ||||
|   const [onClick, loading] = useHandledAsyncCallback(async () => { | ||||
|     if (!experiment.data) return; | ||||
|     await mutation.mutateAsync({ | ||||
|       experimentId: experiment.data.id, | ||||
|       streamScenarios: visibleScenarios, | ||||
|     }); | ||||
|     await utils.promptVariants.list.invalidate(); | ||||
|   }, [mutation]); | ||||
|  | ||||
|   const { canModify } = useExperimentAccess(); | ||||
|   if (!canModify) return <Box w={cellPadding.x} />; | ||||
|  | ||||
|   return ( | ||||
|     <Flex w="100%" justifyContent="flex-end"> | ||||
|       <ActionButton | ||||
|         onClick={onClick} | ||||
|         py={5} | ||||
|         leftIcon={<Icon as={loading ? Spinner : BsPlus} boxSize={6} mr={loading ? 1 : 0} />} | ||||
|       > | ||||
|         <Text display={{ base: "none", md: "flex" }}>Add Variant</Text> | ||||
|       </ActionButton> | ||||
|       {/* <Button | ||||
|         alignItems="center" | ||||
|         justifyContent="center" | ||||
|         fontWeight="normal" | ||||
|         bgColor="transparent" | ||||
|         _hover={{ bgColor: "gray.100" }} | ||||
|         px={cellPadding.x} | ||||
|         onClick={onClick} | ||||
|         height="unset" | ||||
|         minH={headerMinHeight} | ||||
|       > | ||||
|         <Icon as={loading ? Spinner : BsPlus} boxSize={6} mr={loading ? 1 : 0} /> | ||||
|         <Text display={{ base: "none", md: "flex" }}>Add Variant</Text> | ||||
|       </Button> */} | ||||
|     </Flex> | ||||
|   ); | ||||
| } | ||||
| @@ -37,7 +37,6 @@ export const FloatingLabelInput = ({ | ||||
|         borderColor={isFocused ? "blue.500" : "gray.400"} | ||||
|         autoComplete="off" | ||||
|         value={value} | ||||
|         maxHeight={32} | ||||
|         overflowY="auto" | ||||
|         overflowX="hidden" | ||||
|         {...props} | ||||
							
								
								
									
										197
									
								
								app/src/components/OutputsTable/OutputCell/OutputCell.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,197 @@ | ||||
| import { api } from "~/utils/api"; | ||||
| import { type PromptVariant, type Scenario } from "../types"; | ||||
| import { type StackProps, Text, VStack } from "@chakra-ui/react"; | ||||
| import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks"; | ||||
| import SyntaxHighlighter from "react-syntax-highlighter"; | ||||
| import { docco } from "react-syntax-highlighter/dist/cjs/styles/hljs"; | ||||
| import stringify from "json-stringify-pretty-compact"; | ||||
| import { type ReactElement, useState, useEffect, Fragment, useCallback } from "react"; | ||||
| import useSocket from "~/utils/useSocket"; | ||||
| import { OutputStats } from "./OutputStats"; | ||||
| import { RetryCountdown } from "./RetryCountdown"; | ||||
| import frontendModelProviders from "~/modelProviders/frontendModelProviders"; | ||||
| import { ResponseLog } from "./ResponseLog"; | ||||
| import { CellOptions } from "./TopActions"; | ||||
|  | ||||
| const WAITING_MESSAGE_INTERVAL = 20000; | ||||
|  | ||||
| export default function OutputCell({ | ||||
|   scenario, | ||||
|   variant, | ||||
| }: { | ||||
|   scenario: Scenario; | ||||
|   variant: PromptVariant; | ||||
| }): ReactElement | null { | ||||
|   const utils = api.useContext(); | ||||
|   const experiment = useExperiment(); | ||||
|   const vars = api.templateVars.list.useQuery({ | ||||
|     experimentId: experiment.data?.id ?? "", | ||||
|   }).data; | ||||
|  | ||||
|   const scenarioVariables = scenario.variableValues as Record<string, string>; | ||||
|   const templateHasVariables = | ||||
|     vars?.length === 0 || vars?.some((v) => scenarioVariables[v.label] !== undefined); | ||||
|  | ||||
|   let disabledReason: string | null = null; | ||||
|  | ||||
|   if (!templateHasVariables) disabledReason = "Add a value to the scenario variables to see output"; | ||||
|  | ||||
|   const [refetchInterval, setRefetchInterval] = useState(0); | ||||
|   const { data: cell, isLoading: queryLoading } = api.scenarioVariantCells.get.useQuery( | ||||
|     { scenarioId: scenario.id, variantId: variant.id }, | ||||
|     { refetchInterval }, | ||||
|   ); | ||||
|  | ||||
|   const provider = | ||||
|     frontendModelProviders[variant.modelProvider as keyof typeof frontendModelProviders]; | ||||
|  | ||||
|   type OutputSchema = Parameters<typeof provider.normalizeOutput>[0]; | ||||
|  | ||||
|   const { mutateAsync: hardRefetchMutate } = api.scenarioVariantCells.forceRefetch.useMutation(); | ||||
|   const [hardRefetch, hardRefetching] = useHandledAsyncCallback(async () => { | ||||
|     await hardRefetchMutate({ scenarioId: scenario.id, variantId: variant.id }); | ||||
|     await utils.scenarioVariantCells.get.invalidate({ | ||||
|       scenarioId: scenario.id, | ||||
|       variantId: variant.id, | ||||
|     }); | ||||
|     await utils.promptVariants.stats.invalidate({ | ||||
|       variantId: variant.id, | ||||
|     }); | ||||
|   }, [hardRefetchMutate, scenario.id, variant.id]); | ||||
|  | ||||
|   const fetchingOutput = queryLoading || hardRefetching; | ||||
|  | ||||
|   const awaitingOutput = | ||||
|     !cell || | ||||
|     !cell.evalsComplete || | ||||
|     cell.retrievalStatus === "PENDING" || | ||||
|     cell.retrievalStatus === "IN_PROGRESS" || | ||||
|     hardRefetching; | ||||
|   useEffect(() => setRefetchInterval(awaitingOutput ? 1000 : 0), [awaitingOutput]); | ||||
|  | ||||
|   // TODO: disconnect from socket if we're not streaming anymore | ||||
|   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 (!cell && !fetchingOutput) | ||||
|     return ( | ||||
|       <CellWrapper> | ||||
|         <Text color="gray.500">Error retrieving output</Text> | ||||
|       </CellWrapper> | ||||
|     ); | ||||
|  | ||||
|   if (cell && cell.errorMessage) { | ||||
|     return ( | ||||
|       <CellWrapper> | ||||
|         <Text color="red.500">{cell.errorMessage}</Text> | ||||
|       </CellWrapper> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   if (disabledReason) return <Text color="gray.500">{disabledReason}</Text>; | ||||
|  | ||||
|   const showLogs = !streamedMessage && !mostRecentResponse?.output; | ||||
|  | ||||
|   if (showLogs) | ||||
|     return ( | ||||
|       <CellWrapper alignItems="flex-start" fontFamily="inconsolata, monospace" spacing={0}> | ||||
|         {cell?.jobQueuedAt && <ResponseLog time={cell.jobQueuedAt} title="Job queued" />} | ||||
|         {cell?.jobStartedAt && <ResponseLog time={cell.jobStartedAt} title="Job started" />} | ||||
|         {cell?.modelResponses?.map((response) => { | ||||
|           let numWaitingMessages = 0; | ||||
|           const relativeWaitingTime = response.receivedAt | ||||
|             ? response.receivedAt.getTime() | ||||
|             : Date.now(); | ||||
|           if (response.requestedAt) { | ||||
|             numWaitingMessages = Math.floor( | ||||
|               (relativeWaitingTime - response.requestedAt.getTime()) / WAITING_MESSAGE_INTERVAL, | ||||
|             ); | ||||
|           } | ||||
|           return ( | ||||
|             <Fragment key={response.id}> | ||||
|               {response.requestedAt && ( | ||||
|                 <ResponseLog time={response.requestedAt} title="Request sent to API" /> | ||||
|               )} | ||||
|               {response.requestedAt && | ||||
|                 Array.from({ length: numWaitingMessages }, (_, i) => ( | ||||
|                   <ResponseLog | ||||
|                     key={`waiting-${i}`} | ||||
|                     time={ | ||||
|                       new Date( | ||||
|                         (response.requestedAt?.getTime?.() ?? 0) + | ||||
|                           (i + 1) * WAITING_MESSAGE_INTERVAL, | ||||
|                       ) | ||||
|                     } | ||||
|                     title="Waiting for response..." | ||||
|                   /> | ||||
|                 ))} | ||||
|               {response.receivedAt && ( | ||||
|                 <ResponseLog | ||||
|                   time={response.receivedAt} | ||||
|                   title="Response received from API" | ||||
|                   message={`statusCode: ${response.statusCode ?? ""}\n ${ | ||||
|                     response.errorMessage ?? "" | ||||
|                   }`} | ||||
|                 /> | ||||
|               )} | ||||
|             </Fragment> | ||||
|           ); | ||||
|         }) ?? null} | ||||
|         {mostRecentResponse?.retryTime && ( | ||||
|           <RetryCountdown retryTime={mostRecentResponse.retryTime} /> | ||||
|         )} | ||||
|       </CellWrapper> | ||||
|     ); | ||||
|  | ||||
|   const normalizedOutput = mostRecentResponse?.output | ||||
|     ? provider.normalizeOutput(mostRecentResponse?.output) | ||||
|     : streamedMessage | ||||
|     ? provider.normalizeOutput(streamedMessage) | ||||
|     : null; | ||||
|  | ||||
|   if (mostRecentResponse?.output && normalizedOutput?.type === "json") { | ||||
|     return ( | ||||
|       <CellWrapper> | ||||
|         <SyntaxHighlighter | ||||
|           customStyle={{ overflowX: "unset", width: "100%", flex: 1 }} | ||||
|           language="json" | ||||
|           style={docco} | ||||
|           lineProps={{ | ||||
|             style: { wordBreak: "break-all", whiteSpace: "pre-wrap" }, | ||||
|           }} | ||||
|           wrapLines | ||||
|         > | ||||
|           {stringify(normalizedOutput.value, { maxLength: 40 })} | ||||
|         </SyntaxHighlighter> | ||||
|       </CellWrapper> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   const contentToDisplay = (normalizedOutput?.type === "text" && normalizedOutput.value) || ""; | ||||
|  | ||||
|   return ( | ||||
|     <CellWrapper> | ||||
|       <Text>{contentToDisplay}</Text> | ||||
|     </CellWrapper> | ||||
|   ); | ||||
| } | ||||
| @@ -7,28 +7,39 @@ import { CostTooltip } from "~/components/tooltip/CostTooltip"; | ||||
| const SHOW_TIME = true; | ||||
| 
 | ||||
| export const OutputStats = ({ | ||||
|   modelOutput, | ||||
|   modelResponse, | ||||
| }: { | ||||
|   modelOutput: NonNullable< | ||||
|     NonNullable<RouterOutputs["scenarioVariantCells"]["get"]>["modelOutput"] | ||||
|   modelResponse: NonNullable< | ||||
|     NonNullable<RouterOutputs["scenarioVariantCells"]["get"]>["modelResponses"][0] | ||||
|   >; | ||||
|   scenario: Scenario; | ||||
| }) => { | ||||
|   const timeToComplete = modelOutput.timeToComplete; | ||||
|   const timeToComplete = | ||||
|     modelResponse.receivedAt && modelResponse.requestedAt | ||||
|       ? modelResponse.receivedAt.getTime() - modelResponse.requestedAt.getTime() | ||||
|       : 0; | ||||
| 
 | ||||
|   const promptTokens = modelOutput.promptTokens; | ||||
|   const completionTokens = modelOutput.completionTokens; | ||||
|   const promptTokens = modelResponse.promptTokens; | ||||
|   const completionTokens = modelResponse.completionTokens; | ||||
| 
 | ||||
|   return ( | ||||
|     <HStack w="full" align="center" color="gray.500" fontSize="2xs" mt={{ base: 0, md: 1 }}> | ||||
|       <HStack flex={1}> | ||||
|         {modelOutput.outputEvaluation.map((evaluation) => { | ||||
|     <HStack | ||||
|       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) => { | ||||
|           const passed = evaluation.result > 0.5; | ||||
|           return ( | ||||
|             <Tooltip | ||||
|               isDisabled={!evaluation.details} | ||||
|               label={evaluation.details} | ||||
|               key={evaluation.id} | ||||
|               shouldWrapChildren | ||||
|             > | ||||
|               <HStack spacing={0}> | ||||
|                 <Text>{evaluation.evaluation.label}</Text> | ||||
| @@ -42,15 +53,15 @@ export const OutputStats = ({ | ||||
|           ); | ||||
|         })} | ||||
|       </HStack> | ||||
|       {modelOutput.cost && ( | ||||
|       {modelResponse.cost && ( | ||||
|         <CostTooltip | ||||
|           promptTokens={promptTokens} | ||||
|           completionTokens={completionTokens} | ||||
|           cost={modelOutput.cost} | ||||
|           cost={modelResponse.cost} | ||||
|         > | ||||
|           <HStack spacing={0}> | ||||
|             <Icon as={BsCurrencyDollar} /> | ||||
|             <Text mr={1}>{modelOutput.cost.toFixed(3)}</Text> | ||||
|             <Text mr={1}>{modelResponse.cost.toFixed(3)}</Text> | ||||
|           </HStack> | ||||
|         </CostTooltip> | ||||
|       )} | ||||
							
								
								
									
										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> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										22
									
								
								app/src/components/OutputsTable/OutputCell/ResponseLog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,22 @@ | ||||
| import { HStack, VStack, Text } from "@chakra-ui/react"; | ||||
| import dayjs from "dayjs"; | ||||
|  | ||||
| export const ResponseLog = ({ | ||||
|   time, | ||||
|   title, | ||||
|   message, | ||||
| }: { | ||||
|   time: Date; | ||||
|   title: string; | ||||
|   message?: string; | ||||
| }) => { | ||||
|   return ( | ||||
|     <VStack spacing={0} alignItems="flex-start"> | ||||
|       <HStack> | ||||
|         <Text>{dayjs(time).format("HH:mm:ss")}</Text> | ||||
|         <Text>{title}</Text> | ||||
|       </HStack> | ||||
|       {message && <Text pl={4}>{message}</Text>} | ||||
|     </VStack> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,21 +1,12 @@ | ||||
| import { type ScenarioVariantCell } from "@prisma/client"; | ||||
| import { VStack, Text } from "@chakra-ui/react"; | ||||
| import { Text } from "@chakra-ui/react"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import pluralize from "pluralize"; | ||||
| 
 | ||||
| export const ErrorHandler = ({ | ||||
|   cell, | ||||
|   refetchOutput, | ||||
| }: { | ||||
|   cell: ScenarioVariantCell; | ||||
|   refetchOutput: () => void; | ||||
| }) => { | ||||
| export const RetryCountdown = ({ retryTime }: { retryTime: Date }) => { | ||||
|   const [msToWait, setMsToWait] = useState(0); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (!cell.retryTime) return; | ||||
| 
 | ||||
|     const initialWaitTime = cell.retryTime.getTime() - Date.now(); | ||||
|     const initialWaitTime = retryTime.getTime() - Date.now(); | ||||
|     const msModuloOneSecond = initialWaitTime % 1000; | ||||
|     let remainingTime = initialWaitTime - msModuloOneSecond; | ||||
|     setMsToWait(remainingTime); | ||||
| @@ -36,18 +27,13 @@ export const ErrorHandler = ({ | ||||
|       clearInterval(interval); | ||||
|       clearTimeout(timeout); | ||||
|     }; | ||||
|   }, [cell.retryTime, cell.statusCode, setMsToWait, refetchOutput]); | ||||
|   }, [retryTime]); | ||||
| 
 | ||||
|   if (msToWait <= 0) return null; | ||||
| 
 | ||||
|   return ( | ||||
|     <VStack w="full"> | ||||
|       <Text color="red.600" wordBreak="break-word"> | ||||
|         {cell.errorMessage} | ||||
|       </Text> | ||||
|       {msToWait > 0 && ( | ||||
|         <Text color="red.600" fontSize="sm"> | ||||
|           Retrying in {pluralize("second", Math.ceil(msToWait / 1000), true)}... | ||||
|         </Text> | ||||
|       )} | ||||
|     </VStack> | ||||
|     <Text color="red.600" fontSize="sm"> | ||||
|       Retrying in {pluralize("second", Math.ceil(msToWait / 1000), true)}... | ||||
|     </Text> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										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> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										207
									
								
								app/src/components/OutputsTable/ScenarioEditor.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,207 @@ | ||||
| import { isEqual } from "lodash-es"; | ||||
| import { useEffect, useState, type DragEvent } from "react"; | ||||
| import { api } from "~/utils/api"; | ||||
| import { useExperiment, useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks"; | ||||
| import { type Scenario } from "./types"; | ||||
|  | ||||
| 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 { FloatingLabelInput } from "./FloatingLabelInput"; | ||||
| import { ScenarioEditorModal } from "./ScenarioEditorModal"; | ||||
|  | ||||
| export default function ScenarioEditor({ | ||||
|   scenario, | ||||
|   ...props | ||||
| }: { | ||||
|   scenario: Scenario; | ||||
|   hovered: boolean; | ||||
|   canHide: boolean; | ||||
| }) { | ||||
|   const { canModify } = useExperimentAccess(); | ||||
|  | ||||
|   const savedValues = scenario.variableValues as Record<string, string>; | ||||
|   const utils = api.useContext(); | ||||
|   const [isDragTarget, setIsDragTarget] = useState(false); | ||||
|   const [variableInputHovered, setVariableInputHovered] = useState(false); | ||||
|  | ||||
|   const [values, setValues] = useState<Record<string, string>>(savedValues); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (savedValues) setValues(savedValues); | ||||
|   }, [savedValues]); | ||||
|  | ||||
|   const experiment = useExperiment(); | ||||
|   const vars = api.templateVars.list.useQuery({ experimentId: experiment.data?.id ?? "" }); | ||||
|  | ||||
|   const variableLabels = vars.data?.map((v) => v.label) ?? []; | ||||
|  | ||||
|   const hasChanged = !isEqual(savedValues, values); | ||||
|  | ||||
|   const mutation = api.scenarios.replaceWithValues.useMutation(); | ||||
|  | ||||
|   const [onSave] = useHandledAsyncCallback(async () => { | ||||
|     await mutation.mutateAsync({ | ||||
|       id: scenario.id, | ||||
|       values, | ||||
|     }); | ||||
|     await utils.scenarios.list.invalidate(); | ||||
|   }, [mutation, values]); | ||||
|  | ||||
|   const hideMutation = api.scenarios.hide.useMutation(); | ||||
|   const [onHide, hidingInProgress] = useHandledAsyncCallback(async () => { | ||||
|     await hideMutation.mutateAsync({ | ||||
|       id: scenario.id, | ||||
|     }); | ||||
|     await utils.scenarios.list.invalidate(); | ||||
|     await utils.promptVariants.stats.invalidate(); | ||||
|   }, [hideMutation, scenario.id]); | ||||
|  | ||||
|   const reorderMutation = api.scenarios.reorder.useMutation(); | ||||
|   const [onReorder] = useHandledAsyncCallback( | ||||
|     async (e: DragEvent<HTMLDivElement>) => { | ||||
|       e.preventDefault(); | ||||
|       setIsDragTarget(false); | ||||
|       const draggedId = e.dataTransfer.getData("text/plain"); | ||||
|       const droppedId = scenario.id; | ||||
|       if (!draggedId || !droppedId || draggedId === droppedId) return; | ||||
|       await reorderMutation.mutateAsync({ | ||||
|         draggedId, | ||||
|         droppedId, | ||||
|       }); | ||||
|       await utils.scenarios.list.invalidate(); | ||||
|     }, | ||||
|     [reorderMutation, scenario.id], | ||||
|   ); | ||||
|  | ||||
|   const [scenarioEditorModalOpen, setScenarioEditorModalOpen] = useState(false); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <HStack | ||||
|         alignItems="flex-start" | ||||
|         px={cellPadding.x} | ||||
|         py={cellPadding.y} | ||||
|         spacing={0} | ||||
|         height="100%" | ||||
|         draggable={!variableInputHovered} | ||||
|         onDragStart={(e) => { | ||||
|           e.dataTransfer.setData("text/plain", scenario.id); | ||||
|           e.currentTarget.style.opacity = "0.4"; | ||||
|         }} | ||||
|         onDragEnd={(e) => { | ||||
|           e.currentTarget.style.opacity = "1"; | ||||
|         }} | ||||
|         onDragOver={(e) => { | ||||
|           e.preventDefault(); | ||||
|           setIsDragTarget(true); | ||||
|         }} | ||||
|         onDragLeave={() => { | ||||
|           setIsDragTarget(false); | ||||
|         }} | ||||
|         onDrop={onReorder} | ||||
|         backgroundColor={isDragTarget ? "gray.100" : "transparent"} | ||||
|       > | ||||
|         {variableLabels.length === 0 ? ( | ||||
|           <Box color="gray.500"> | ||||
|             {vars.data ? "No scenario variables configured" : "Loading..."} | ||||
|           </Box> | ||||
|         ) : ( | ||||
|           <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) => { | ||||
|               const value = values[key] ?? ""; | ||||
|               return ( | ||||
|                 <FloatingLabelInput | ||||
|                   key={key} | ||||
|                   label={key} | ||||
|                   isDisabled={!canModify} | ||||
|                   style={{ width: "100%" }} | ||||
|                   maxHeight={32} | ||||
|                   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(); | ||||
|                     } | ||||
|                   }} | ||||
|                   onMouseEnter={() => setVariableInputHovered(true)} | ||||
|                   onMouseLeave={() => setVariableInputHovered(false)} | ||||
|                 /> | ||||
|               ); | ||||
|             })} | ||||
|             {hasChanged && ( | ||||
|               <HStack justify="right"> | ||||
|                 <Button | ||||
|                   size="sm" | ||||
|                   onMouseDown={() => { | ||||
|                     setValues(savedValues); | ||||
|                   }} | ||||
|                   colorScheme="gray" | ||||
|                 > | ||||
|                   Reset | ||||
|                 </Button> | ||||
|                 <Button size="sm" onMouseDown={onSave} colorScheme="blue"> | ||||
|                   Save | ||||
|                 </Button> | ||||
|               </HStack> | ||||
|             )} | ||||
|           </VStack> | ||||
|         )} | ||||
|       </HStack> | ||||
|       {scenarioEditorModalOpen && ( | ||||
|         <ScenarioEditorModal | ||||
|           scenarioId={scenario.id} | ||||
|           initialValues={savedValues} | ||||
|           onClose={() => setScenarioEditorModalOpen(false)} | ||||
|         /> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										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> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										21
									
								
								app/src/components/OutputsTable/ScenarioPaginator.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| import { useScenarios } from "~/utils/hooks"; | ||||
| import Paginator from "../Paginator"; | ||||
|  | ||||
| const ScenarioPaginator = () => { | ||||
|   const { data } = useScenarios(); | ||||
|  | ||||
|   if (!data) return null; | ||||
|  | ||||
|   const { scenarios, startIndex, lastPage, count } = data; | ||||
|  | ||||
|   return ( | ||||
|     <Paginator | ||||
|       numItemsLoaded={scenarios.length} | ||||
|       startIndex={startIndex} | ||||
|       lastPage={lastPage} | ||||
|       count={count} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default ScenarioPaginator; | ||||
| @@ -1,14 +1,15 @@ | ||||
| import { Box, GridItem } from "@chakra-ui/react"; | ||||
| import { GridItem } from "@chakra-ui/react"; | ||||
| import React, { useState } from "react"; | ||||
| import { cellPadding } from "../constants"; | ||||
| import OutputCell from "./OutputCell/OutputCell"; | ||||
| import ScenarioEditor from "./ScenarioEditor"; | ||||
| import type { PromptVariant, Scenario } from "./types"; | ||||
| import { borders } from "./styles"; | ||||
| 
 | ||||
| const ScenarioRow = (props: { | ||||
|   scenario: Scenario; | ||||
|   variants: PromptVariant[]; | ||||
|   canHide: boolean; | ||||
|   rowStart: number; | ||||
| }) => { | ||||
|   const [isHovered, setIsHovered] = useState(false); | ||||
| 
 | ||||
| @@ -21,19 +22,23 @@ const ScenarioRow = (props: { | ||||
|         onMouseLeave={() => setIsHovered(false)} | ||||
|         sx={isHovered ? highlightStyle : undefined} | ||||
|         borderLeftWidth={1} | ||||
|         {...borders} | ||||
|         rowStart={props.rowStart} | ||||
|         colStart={1} | ||||
|       > | ||||
|         <ScenarioEditor scenario={props.scenario} hovered={isHovered} canHide={props.canHide} /> | ||||
|       </GridItem> | ||||
|       {props.variants.map((variant) => ( | ||||
|       {props.variants.map((variant, i) => ( | ||||
|         <GridItem | ||||
|           key={variant.id} | ||||
|           onMouseEnter={() => setIsHovered(true)} | ||||
|           onMouseLeave={() => setIsHovered(false)} | ||||
|           sx={isHovered ? highlightStyle : undefined} | ||||
|           rowStart={props.rowStart} | ||||
|           colStart={i + 2} | ||||
|           {...borders} | ||||
|         > | ||||
|           <Box h="100%" w="100%" px={cellPadding.x} py={cellPadding.y}> | ||||
|             <OutputCell key={variant.id} scenario={props.scenario} variant={variant} /> | ||||
|           </Box> | ||||
|           <OutputCell key={variant.id} scenario={props.scenario} variant={variant} /> | ||||
|         </GridItem> | ||||
|       ))} | ||||
|     </> | ||||
							
								
								
									
										82
									
								
								app/src/components/OutputsTable/ScenariosHeader.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,82 @@ | ||||
| import { | ||||
|   Button, | ||||
|   type ButtonProps, | ||||
|   HStack, | ||||
|   Text, | ||||
|   Icon, | ||||
|   Menu, | ||||
|   MenuButton, | ||||
|   MenuList, | ||||
|   MenuItem, | ||||
|   IconButton, | ||||
|   Spinner, | ||||
| } from "@chakra-ui/react"; | ||||
| import { cellPadding } from "../constants"; | ||||
| import { | ||||
|   useExperiment, | ||||
|   useExperimentAccess, | ||||
|   useHandledAsyncCallback, | ||||
|   useScenarios, | ||||
| } from "~/utils/hooks"; | ||||
| import { BsGear, BsPencil, BsPlus, BsStars } from "react-icons/bs"; | ||||
| import { useAppStore } from "~/state/store"; | ||||
| import { api } from "~/utils/api"; | ||||
|  | ||||
| export const ActionButton = (props: ButtonProps) => ( | ||||
|   <Button size="sm" variant="ghost" color="gray.600" {...props} /> | ||||
| ); | ||||
|  | ||||
| export const ScenariosHeader = () => { | ||||
|   const openDrawer = useAppStore((s) => s.openDrawer); | ||||
|   const { canModify } = useExperimentAccess(); | ||||
|   const scenarios = useScenarios(); | ||||
|  | ||||
|   const experiment = useExperiment(); | ||||
|   const createScenarioMutation = api.scenarios.create.useMutation(); | ||||
|   const utils = api.useContext(); | ||||
|  | ||||
|   const [onAddScenario, loading] = useHandledAsyncCallback( | ||||
|     async (autogenerate: boolean) => { | ||||
|       if (!experiment.data) return; | ||||
|       await createScenarioMutation.mutateAsync({ | ||||
|         experimentId: experiment.data.id, | ||||
|         autogenerate, | ||||
|       }); | ||||
|       await utils.scenarios.list.invalidate(); | ||||
|     }, | ||||
|     [createScenarioMutation], | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <HStack w="100%" pb={cellPadding.y} pt={0} align="center" spacing={0}> | ||||
|       <Text fontSize={16} fontWeight="bold"> | ||||
|         Scenarios ({scenarios.data?.count}) | ||||
|       </Text> | ||||
|       {canModify && ( | ||||
|         <Menu> | ||||
|           <MenuButton | ||||
|             as={IconButton} | ||||
|             mt={1} | ||||
|             variant="ghost" | ||||
|             aria-label="Edit Scenarios" | ||||
|             icon={<Icon as={loading ? Spinner : BsGear} />} | ||||
|           /> | ||||
|           <MenuList fontSize="md" zIndex="dropdown" mt={-3}> | ||||
|             <MenuItem | ||||
|               icon={<Icon as={BsPlus} boxSize={6} mx="-5px" />} | ||||
|               onClick={() => onAddScenario(false)} | ||||
|             > | ||||
|               Add Scenario | ||||
|             </MenuItem> | ||||
|             <MenuItem icon={<BsStars />} onClick={() => onAddScenario(true)}> | ||||
|               Autogenerate Scenario | ||||
|             </MenuItem> | ||||
|             <MenuItem icon={<BsPencil />} onClick={openDrawer}> | ||||
|               Edit Vars | ||||
|             </MenuItem> | ||||
|           </MenuList> | ||||
|         </Menu> | ||||
|       )} | ||||
|     </HStack> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,18 +1,53 @@ | ||||
| import { Box, Button, HStack, Spinner, Tooltip, useToast, Text } from "@chakra-ui/react"; | ||||
| import { useRef, useEffect, useState, useCallback } from "react"; | ||||
| import { useExperimentAccess, useHandledAsyncCallback, useModifierKeyLabel } from "~/utils/hooks"; | ||||
| import { type PromptVariant } from "./types"; | ||||
| import { api } from "~/utils/api"; | ||||
| import { | ||||
|   Box, | ||||
|   Button, | ||||
|   HStack, | ||||
|   IconButton, | ||||
|   Spinner, | ||||
|   Text, | ||||
|   Tooltip, | ||||
|   useToast, | ||||
| } from "@chakra-ui/react"; | ||||
| import { useCallback, useEffect, useRef, useState } from "react"; | ||||
| import { FiMaximize, FiMinimize } from "react-icons/fi"; | ||||
| import { editorBackground } from "~/state/sharedVariantEditor.slice"; | ||||
| import { useAppStore } from "~/state/store"; | ||||
| import { api } from "~/utils/api"; | ||||
| import { | ||||
|   useExperimentAccess, | ||||
|   useHandledAsyncCallback, | ||||
|   useModifierKeyLabel, | ||||
|   useVisibleScenarioIds, | ||||
| } from "~/utils/hooks"; | ||||
| import { type PromptVariant } from "./types"; | ||||
| 
 | ||||
| export default function VariantEditor(props: { variant: PromptVariant }) { | ||||
|   const { canModify } = useExperimentAccess(); | ||||
|   const monaco = useAppStore.use.sharedVariantEditor.monaco(); | ||||
|   const editorRef = useRef<ReturnType<NonNullable<typeof monaco>["editor"]["create"]> | null>(null); | ||||
|   const containerRef = useRef<HTMLDivElement | null>(null); | ||||
|   const [editorId] = useState(() => `editor_${Math.random().toString(36).substring(7)}`); | ||||
|   const [isChanged, setIsChanged] = useState(false); | ||||
| 
 | ||||
|   const lastSavedFn = props.variant.constructFn; | ||||
|   const [isFullscreen, setIsFullscreen] = useState(false); | ||||
| 
 | ||||
|   const toggleFullscreen = useCallback(() => { | ||||
|     setIsFullscreen((prev) => !prev); | ||||
|     editorRef.current?.focus(); | ||||
|   }, [setIsFullscreen]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     const handleEsc = (event: KeyboardEvent) => { | ||||
|       if (event.key === "Escape" && isFullscreen) { | ||||
|         toggleFullscreen(); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     window.addEventListener("keydown", handleEsc); | ||||
|     return () => window.removeEventListener("keydown", handleEsc); | ||||
|   }, [isFullscreen, toggleFullscreen]); | ||||
| 
 | ||||
|   const lastSavedFn = props.variant.promptConstructor; | ||||
| 
 | ||||
|   const modifierKey = useModifierKeyLabel(); | ||||
| 
 | ||||
| @@ -33,6 +68,7 @@ export default function VariantEditor(props: { variant: PromptVariant }) { | ||||
|   const replaceVariant = api.promptVariants.replaceVariant.useMutation(); | ||||
|   const utils = api.useContext(); | ||||
|   const toast = useToast(); | ||||
|   const visibleScenarios = useVisibleScenarioIds(); | ||||
| 
 | ||||
|   const [onSave, saveInProgress] = useHandledAsyncCallback(async () => { | ||||
|     if (!editorRef.current) return; | ||||
| @@ -60,7 +96,8 @@ export default function VariantEditor(props: { variant: PromptVariant }) { | ||||
| 
 | ||||
|     const resp = await replaceVariant.mutateAsync({ | ||||
|       id: props.variant.id, | ||||
|       constructFn: currentFn, | ||||
|       promptConstructor: currentFn, | ||||
|       streamScenarios: visibleScenarios, | ||||
|     }); | ||||
|     if (resp.status === "error") { | ||||
|       return toast({ | ||||
| @@ -99,11 +136,23 @@ export default function VariantEditor(props: { variant: PromptVariant }) { | ||||
|         readOnly: !canModify, | ||||
|       }); | ||||
| 
 | ||||
|       // Workaround because otherwise the commands only work on whatever
 | ||||
|       // editor was loaded on the page last.
 | ||||
|       // https://github.com/microsoft/monaco-editor/issues/2947#issuecomment-1422265201
 | ||||
|       editorRef.current.onDidFocusEditorText(() => { | ||||
|         // Workaround because otherwise the command only works on whatever
 | ||||
|         // editor was loaded on the page last.
 | ||||
|         // https://github.com/microsoft/monaco-editor/issues/2947#issuecomment-1422265201
 | ||||
|         editorRef.current?.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, onSave); | ||||
|         editorRef.current?.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, onSave); | ||||
| 
 | ||||
|         editorRef.current?.addCommand( | ||||
|           monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, | ||||
|           toggleFullscreen, | ||||
|         ); | ||||
| 
 | ||||
|         // Exit fullscreen with escape
 | ||||
|         editorRef.current?.addCommand(monaco.KeyCode.Escape, () => { | ||||
|           if (isFullscreen) { | ||||
|             toggleFullscreen(); | ||||
|           } | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       editorRef.current.onDidChangeModelContent(checkForChanges); | ||||
| @@ -132,8 +181,40 @@ export default function VariantEditor(props: { variant: PromptVariant }) { | ||||
|   }, [canModify]); | ||||
| 
 | ||||
|   return ( | ||||
|     <Box w="100%" pos="relative"> | ||||
|       <div id={editorId} style={{ height: "400px", width: "100%" }}></div> | ||||
|     <Box | ||||
|       w="100%" | ||||
|       ref={containerRef} | ||||
|       sx={ | ||||
|         isFullscreen | ||||
|           ? { | ||||
|               position: "fixed", | ||||
|               top: 0, | ||||
|               left: 0, | ||||
|               right: 0, | ||||
|               bottom: 0, | ||||
|             } | ||||
|           : { h: "400px", w: "100%" } | ||||
|       } | ||||
|       bgColor={editorBackground} | ||||
|       zIndex={isFullscreen ? 1000 : "unset"} | ||||
|       pos="relative" | ||||
|       _hover={{ ".fullscreen-toggle": { opacity: 1 } }} | ||||
|     > | ||||
|       <Box id={editorId} w="100%" h="100%" /> | ||||
|       <Tooltip label={`${modifierKey} + ⇧ + F`}> | ||||
|         <IconButton | ||||
|           className="fullscreen-toggle" | ||||
|           aria-label="Minimize" | ||||
|           icon={isFullscreen ? <FiMinimize /> : <FiMaximize />} | ||||
|           position="absolute" | ||||
|           top={2} | ||||
|           right={2} | ||||
|           onClick={toggleFullscreen} | ||||
|           opacity={0} | ||||
|           transition="opacity 0.2s" | ||||
|         /> | ||||
|       </Tooltip> | ||||
| 
 | ||||
|       {isChanged && ( | ||||
|         <HStack pos="absolute" bottom={2} right={2}> | ||||
|           <Button | ||||
| @@ -146,7 +227,7 @@ export default function VariantEditor(props: { variant: PromptVariant }) { | ||||
|           > | ||||
|             Reset | ||||
|           </Button> | ||||
|           <Tooltip label={`${modifierKey} + Enter`}> | ||||
|           <Tooltip label={`${modifierKey} + S`}> | ||||
|             <Button size="sm" onClick={onSave} colorScheme="blue" w={16} disabled={saveInProgress}> | ||||
|               {saveInProgress ? <Spinner boxSize={4} /> : <Text>Save</Text>} | ||||
|             </Button> | ||||
| @@ -21,17 +21,14 @@ export default function VariantStats(props: { variant: PromptVariant }) { | ||||
|         completionTokens: 0, | ||||
|         scenarioCount: 0, | ||||
|         outputCount: 0, | ||||
|         awaitingRetrievals: false, | ||||
|         awaitingEvals: false, | ||||
|       }, | ||||
|       refetchInterval, | ||||
|     }, | ||||
|   ); | ||||
| 
 | ||||
|   // Poll every two seconds while we are waiting for LLM retrievals to finish
 | ||||
|   useEffect( | ||||
|     () => setRefetchInterval(data.awaitingRetrievals ? 2000 : 0), | ||||
|     [data.awaitingRetrievals], | ||||
|   ); | ||||
|   useEffect(() => setRefetchInterval(data.awaitingEvals ? 5000 : 0), [data.awaitingEvals]); | ||||
| 
 | ||||
|   const [passColor, neutralColor, failColor] = useToken("colors", [ | ||||
|     "green.500", | ||||
| @@ -46,17 +43,17 @@ export default function VariantStats(props: { variant: PromptVariant }) { | ||||
|   return ( | ||||
|     <HStack | ||||
|       justifyContent="space-between" | ||||
|       alignItems="center" | ||||
|       alignItems="flex-end" | ||||
|       mx="2" | ||||
|       fontSize="xs" | ||||
|       py={cellPadding.y} | ||||
|     > | ||||
|       {showNumFinished && ( | ||||
|         <Text> | ||||
|           {data.outputCount} / {data.scenarioCount} | ||||
|         </Text> | ||||
|       )} | ||||
|       <HStack px={cellPadding.x}> | ||||
|       <HStack px={cellPadding.x} flexWrap="wrap"> | ||||
|         {showNumFinished && ( | ||||
|           <Text> | ||||
|             {data.outputCount} / {data.scenarioCount} | ||||
|           </Text> | ||||
|         )} | ||||
|         {data.evalResults.map((result) => { | ||||
|           const passedFrac = result.passCount / result.totalCount; | ||||
|           return ( | ||||
| @@ -69,7 +66,7 @@ export default function VariantStats(props: { variant: PromptVariant }) { | ||||
|           ); | ||||
|         })} | ||||
|       </HStack> | ||||
|       {data.overallCost && !data.awaitingRetrievals && ( | ||||
|       {data.overallCost && ( | ||||
|         <CostTooltip | ||||
|           promptTokens={data.promptTokens} | ||||
|           completionTokens={data.completionTokens} | ||||
							
								
								
									
										107
									
								
								app/src/components/OutputsTable/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,107 @@ | ||||
| import { Grid, GridItem, type GridItemProps } from "@chakra-ui/react"; | ||||
| import { api } from "~/utils/api"; | ||||
| import AddVariantButton from "./AddVariantButton"; | ||||
| import ScenarioRow from "./ScenarioRow"; | ||||
| import VariantEditor from "./VariantEditor"; | ||||
| import VariantHeader from "../VariantHeader/VariantHeader"; | ||||
| import VariantStats from "./VariantStats"; | ||||
| import { ScenariosHeader } from "./ScenariosHeader"; | ||||
| import { borders } from "./styles"; | ||||
| import { useScenarios } from "~/utils/hooks"; | ||||
| import ScenarioPaginator from "./ScenarioPaginator"; | ||||
| import { Fragment } from "react"; | ||||
|  | ||||
| export default function OutputsTable({ experimentId }: { experimentId: string | undefined }) { | ||||
|   const variants = api.promptVariants.list.useQuery( | ||||
|     { experimentId: experimentId as string }, | ||||
|     { enabled: !!experimentId }, | ||||
|   ); | ||||
|  | ||||
|   const scenarios = useScenarios(); | ||||
|  | ||||
|   if (!variants.data || !scenarios.data) return null; | ||||
|  | ||||
|   const allCols = variants.data.length + 2; | ||||
|   const variantHeaderRows = 3; | ||||
|   const scenarioHeaderRows = 1; | ||||
|   const scenarioFooterRows = 1; | ||||
|   const visibleScenariosCount = scenarios.data.scenarios.length; | ||||
|   const allRows = | ||||
|     variantHeaderRows + scenarioHeaderRows + visibleScenariosCount + scenarioFooterRows; | ||||
|  | ||||
|   return ( | ||||
|     <Grid | ||||
|       pt={4} | ||||
|       pb={24} | ||||
|       pl={8} | ||||
|       display="grid" | ||||
|       gridTemplateColumns={`250px repeat(${variants.data.length}, minmax(320px, 1fr)) auto`} | ||||
|       sx={{ | ||||
|         "> *": { | ||||
|           borderColor: "gray.300", | ||||
|         }, | ||||
|       }} | ||||
|       fontSize="sm" | ||||
|     > | ||||
|       <GridItem rowSpan={variantHeaderRows}> | ||||
|         <AddVariantButton /> | ||||
|       </GridItem> | ||||
|  | ||||
|       {variants.data.map((variant, i) => { | ||||
|         const sharedProps: GridItemProps = { | ||||
|           ...borders, | ||||
|           colStart: i + 2, | ||||
|           borderLeftWidth: i === 0 ? 1 : 0, | ||||
|           marginLeft: i === 0 ? "-1px" : 0, | ||||
|           backgroundColor: "gray.100", | ||||
|         }; | ||||
|         return ( | ||||
|           <Fragment key={variant.uiId}> | ||||
|             <VariantHeader | ||||
|               variant={variant} | ||||
|               canHide={variants.data.length > 1} | ||||
|               rowStart={1} | ||||
|               {...sharedProps} | ||||
|             /> | ||||
|             <GridItem rowStart={2} {...sharedProps}> | ||||
|               <VariantEditor variant={variant} /> | ||||
|             </GridItem> | ||||
|             <GridItem rowStart={3} {...sharedProps}> | ||||
|               <VariantStats variant={variant} /> | ||||
|             </GridItem> | ||||
|           </Fragment> | ||||
|         ); | ||||
|       })} | ||||
|  | ||||
|       <GridItem | ||||
|         colSpan={allCols - 1} | ||||
|         rowStart={variantHeaderRows + 1} | ||||
|         colStart={1} | ||||
|         {...borders} | ||||
|         borderRightWidth={0} | ||||
|       > | ||||
|         <ScenariosHeader /> | ||||
|       </GridItem> | ||||
|  | ||||
|       {scenarios.data.scenarios.map((scenario, i) => ( | ||||
|         <ScenarioRow | ||||
|           rowStart={i + variantHeaderRows + scenarioHeaderRows + 2} | ||||
|           key={scenario.uiId} | ||||
|           scenario={scenario} | ||||
|           variants={variants.data} | ||||
|           canHide={visibleScenariosCount > 1} | ||||
|         /> | ||||
|       ))} | ||||
|       <GridItem | ||||
|         rowStart={variantHeaderRows + scenarioHeaderRows + visibleScenariosCount + 2} | ||||
|         colStart={1} | ||||
|         colSpan={allCols} | ||||
|       > | ||||
|         <ScenarioPaginator /> | ||||
|       </GridItem> | ||||
|  | ||||
|       {/* Add some extra padding on the right, because when the table is too wide to fit in the viewport `pr` on the Grid isn't respected. */} | ||||
|       <GridItem rowStart={1} colStart={allCols} rowSpan={allRows} w={4} borderBottomWidth={0} /> | ||||
|     </Grid> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										6
									
								
								app/src/components/OutputsTable/styles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| import { type GridItemProps } from "@chakra-ui/react"; | ||||
|  | ||||
| export const borders: GridItemProps = { | ||||
|   borderRightWidth: 1, | ||||
|   borderBottomWidth: 1, | ||||
| }; | ||||
| @@ -2,4 +2,4 @@ import { type RouterOutputs } from "~/utils/api"; | ||||
| 
 | ||||
| export type PromptVariant = NonNullable<RouterOutputs["promptVariants"]["list"]>[0]; | ||||
| 
 | ||||
| export type Scenario = NonNullable<RouterOutputs["scenarios"]["list"]>[0]; | ||||
| export type Scenario = NonNullable<RouterOutputs["scenarios"]["list"]>["scenarios"][0]; | ||||
							
								
								
									
										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; | ||||
| @@ -1,22 +1,23 @@ | ||||
| import { HStack, Icon, Heading, Text, VStack, GridItem } from "@chakra-ui/react"; | ||||
| import { type IconType } from "react-icons"; | ||||
| import { refineOptions, type RefineOptionLabel } from "./refineOptions"; | ||||
| import { BsStars } from "react-icons/bs"; | ||||
| 
 | ||||
| export const RefineOption = ({ | ||||
| export const RefineAction = ({ | ||||
|   label, | ||||
|   activeLabel, | ||||
|   icon, | ||||
|   desciption, | ||||
|   activeLabel, | ||||
|   onClick, | ||||
|   loading, | ||||
| }: { | ||||
|   label: RefineOptionLabel; | ||||
|   activeLabel: RefineOptionLabel | undefined; | ||||
|   icon: IconType; | ||||
|   onClick: (label: RefineOptionLabel) => void; | ||||
|   label: string; | ||||
|   icon?: IconType; | ||||
|   desciption: string; | ||||
|   activeLabel: string | undefined; | ||||
|   onClick: (label: string) => void; | ||||
|   loading: boolean; | ||||
| }) => { | ||||
|   const isActive = activeLabel === label; | ||||
|   const desciption = refineOptions[label].description; | ||||
| 
 | ||||
|   return ( | ||||
|     <GridItem w="80" h="44"> | ||||
| @@ -44,7 +45,7 @@ export const RefineOption = ({ | ||||
|         opacity={loading ? 0.5 : 1} | ||||
|       > | ||||
|         <HStack cursor="pointer" spacing={6} fontSize="sm" fontWeight="medium" color="gray.500"> | ||||
|           <Icon as={icon} boxSize={12} /> | ||||
|           <Icon as={icon || BsStars} boxSize={12} /> | ||||
|           <Heading size="md" fontFamily="inconsolata, monospace"> | ||||
|             {label} | ||||
|           </Heading> | ||||