diff --git a/@types/nextjs-routes.d.ts b/@types/nextjs-routes.d.ts new file mode 100644 index 0000000..7e51613 --- /dev/null +++ b/@types/nextjs-routes.d.ts @@ -0,0 +1,176 @@ +// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +// This file will be automatically regenerated when your Next.js server is running. +// nextjs-routes version: 2.0.1 +/* eslint-disable */ + +// prettier-ignore +declare module "nextjs-routes" { + import type { + GetServerSidePropsContext as NextGetServerSidePropsContext, + GetServerSidePropsResult as NextGetServerSidePropsResult + } from "next"; + + export type Route = + | DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }> + | DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }> + | DynamicRoute<"/experiments/[id]", { "id": string }>; + + interface StaticRoute { + pathname: Pathname; + query?: Query | undefined; + hash?: string | null | undefined; + } + + interface DynamicRoute { + pathname: Pathname; + query: Parameters & Query; + hash?: string | null | undefined; + } + + interface Query { + [key: string]: string | string[] | undefined; + }; + + export type RoutedQuery

= Extract< + Route, + { pathname: P } + >["query"]; + + export type Locale = + | "en"; + + /** + * A typesafe utility function for generating paths in your application. + * + * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". + */ + export declare function route(r: Route): string; + + /** + * Nearly identical to GetServerSidePropsContext from next, but further narrows + * types based on nextjs-route's route data. + */ + export type GetServerSidePropsContext< + Pathname extends Route["pathname"] = Route["pathname"], + Preview extends NextGetServerSidePropsContext["previewData"] = NextGetServerSidePropsContext["previewData"] + > = Omit & { + params: Extract["query"]; + query: Query; + defaultLocale: "en"; + locale: Locale; + locales: [ + "en" + ]; + }; + + /** + * Nearly identical to GetServerSideProps from next, but further narrows + * types based on nextjs-route's route data. + */ + export type GetServerSideProps< + Props extends { [key: string]: any } = { [key: string]: any }, + Pathname extends Route["pathname"] = Route["pathname"], + Preview extends NextGetServerSideProps["previewData"] = NextGetServerSideProps["previewData"] + > = ( + context: GetServerSidePropsContext + ) => Promise> +} + +// prettier-ignore +declare module "next/link" { + import type { Route } from "nextjs-routes"; + import type { LinkProps as NextLinkProps } from "next/dist/client/link"; + import type { + AnchorHTMLAttributes, + DetailedReactHTMLElement, + MouseEventHandler, + PropsWithChildren, + } from "react"; + export * from "next/dist/client/link"; + + type StaticRoute = Exclude["pathname"]; + + export interface LinkProps + extends Omit, + AnchorHTMLAttributes { + href: Route | StaticRoute | Omit + locale?: Locale | false; + } + + type LinkReactElement = DetailedReactHTMLElement< + { + onMouseEnter?: MouseEventHandler | undefined; + onClick: MouseEventHandler; + href?: string | undefined; + ref?: any; + }, + HTMLElement + >; + + declare function Link(props: PropsWithChildren): LinkReactElement; + + export default Link; +} + +// prettier-ignore +declare module "next/router" { + import type { Locale, Route, RoutedQuery } from "nextjs-routes"; + import type { NextRouter as Router } from "next/dist/client/router"; + export * from "next/dist/client/router"; + export { default } from "next/dist/client/router"; + + type NextTransitionOptions = NonNullable[2]>; + type StaticRoute = Exclude["pathname"]; + + interface TransitionOptions extends Omit { + locale?: Locale | false; + } + + type PathnameAndQuery = Required< + Pick, "pathname" | "query"> + >; + + type AutomaticStaticOptimizedQuery = Omit & { + query: Partial; + }; + + type BaseRouter = + | ({ isReady: false } & AutomaticStaticOptimizedQuery) + | ({ isReady: true } & PaQ); + + export type NextRouter

= + BaseRouter> & + Omit< + Router, + | "defaultLocale" + | "domainLocales" + | "isReady" + | "locale" + | "locales" + | "pathname" + | "push" + | "query" + | "replace" + | "route" + > & { + defaultLocale: "en"; + domainLocales?: undefined; + locale: Locale; + locales: [ + "en" + ]; + push( + url: Route | StaticRoute | Omit, + as?: string, + options?: TransitionOptions + ): Promise; + replace( + url: Route | StaticRoute | Omit, + as?: string, + options?: TransitionOptions + ): Promise; + route: P; + }; + + export function useRouter

(): NextRouter

; +} diff --git a/next.config.mjs b/next.config.mjs index 654e725..82d918d 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,3 +1,5 @@ +import nextRoutes from "nextjs-routes/config"; + /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful * for Docker builds. @@ -20,4 +22,4 @@ const config = { }, }; -export default config; +export default nextRoutes()(config); diff --git a/package.json b/package.json index 5fcedb2..9b88257 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "lodash": "^4.17.21", "next": "^13.4.2", "next-auth": "^4.22.1", + "nextjs-routes": "^2.0.1", "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "^4.10.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e3f488..bf73dcd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ dependencies: next-auth: specifier: ^4.22.1 version: 4.22.1(next@13.4.2)(react-dom@18.2.0)(react@18.2.0) + nextjs-routes: + specifier: ^2.0.1 + version: 2.0.1(next@13.4.2) react: specifier: 18.2.0 version: 18.2.0 @@ -2256,6 +2259,14 @@ packages: color-convert: 2.0.1 dev: true + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: false + /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true @@ -2364,6 +2375,11 @@ packages: engines: {node: '>=0.6'} dev: true + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: false + /bplist-parser@0.2.0: resolution: {integrity: sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==} engines: {node: '>= 5.10.0'} @@ -2383,7 +2399,6 @@ packages: engines: {node: '>=8'} dependencies: fill-range: 7.0.1 - dev: true /buffer-from@0.1.2: resolution: {integrity: sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==} @@ -2438,6 +2453,21 @@ packages: supports-color: 7.2.0 dev: true + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: false + /client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} dev: false @@ -3117,7 +3147,6 @@ packages: engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 - dev: true /find-root@1.1.0: resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} @@ -3246,7 +3275,6 @@ packages: engines: {node: '>= 6'} dependencies: is-glob: 4.0.3 - dev: true /glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} @@ -3466,6 +3494,13 @@ packages: has-bigints: 1.0.2 dev: true + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: false + /is-boolean-object@1.1.2: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} engines: {node: '>= 0.4'} @@ -3506,14 +3541,12 @@ packages: /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - dev: true /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} dependencies: is-extglob: 2.1.1 - dev: true /is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} @@ -3538,7 +3571,6 @@ packages: /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - dev: true /is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} @@ -3867,6 +3899,21 @@ packages: - babel-plugin-macros dev: false + /nextjs-routes@2.0.1(next@13.4.2): + resolution: {integrity: sha512-pBGRm6uR44zwUjWWYn6+gwz08BhBbqUYlIzsbNHAh1TWohHYKWFaa2YVsj8BxEo726MZYg87OJPnHpaaY1ia0w==} + hasBin: true + peerDependencies: + next: '*' + dependencies: + chokidar: 3.5.3 + next: 13.4.2(react-dom@18.2.0)(react@18.2.0) + dev: false + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: false + /npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -4083,7 +4130,6 @@ packages: /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - dev: true /postcss@8.4.14: resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==} @@ -4305,6 +4351,13 @@ packages: util-deprecate: 1.0.2 dev: false + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: false + /regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} @@ -4636,7 +4689,6 @@ packages: engines: {node: '>=8.0'} dependencies: is-number: 7.0.0 - dev: true /toggle-selection@1.0.6: resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e1956f1..a466394 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -15,6 +15,8 @@ model Experiment { id String @id @default(uuid()) @db.Uuid label String + sortIndex Int @default(0) + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt TemplateVariable TemplateVariable[] @@ -33,7 +35,7 @@ model PromptVariant { sortIndex Int @default(0) experimentId String @db.Uuid - experiment Experiment @relation(fields: [experimentId], references: [id]) + experiment Experiment @relation(fields: [experimentId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -52,7 +54,7 @@ model TestScenario { sortIndex Int @default(0) experimentId String @db.Uuid - experiment Experiment @relation(fields: [experimentId], references: [id]) + experiment Experiment @relation(fields: [experimentId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -65,7 +67,7 @@ model TemplateVariable { label String experimentId String @db.Uuid - experiment Experiment @relation(fields: [experimentId], references: [id]) + experiment Experiment @relation(fields: [experimentId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -78,10 +80,10 @@ model ModelOutput { output Json promptVariantId String @db.Uuid - promptVariant PromptVariant @relation(fields: [promptVariantId], references: [id]) + promptVariant PromptVariant @relation(fields: [promptVariantId], references: [id], onDelete: Cascade) testScenarioId String @db.Uuid - testScenario TestScenario @relation(fields: [testScenarioId], references: [id]) + testScenario TestScenario @relation(fields: [testScenarioId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/prisma/seed.ts b/prisma/seed.ts index b19b1a0..9fad9ef 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,14 +1,18 @@ import { prisma } from "~/server/db"; const experimentId = "11111111-1111-1111-1111-111111111111"; -const experiment = await prisma.experiment.upsert({ + +// Delete the existing experiment +await prisma.experiment.delete({ where: { id: experimentId, }, - update: {}, - create: { +}); + +const experiment = await prisma.experiment.create({ + data: { id: experimentId, - label: "Quick Start", + label: "Country Capitals Example", }, }); @@ -34,7 +38,7 @@ const resp = await prisma.promptVariant.createMany({ sortIndex: 0, config: { model: "gpt-3.5-turbo", - messages: [{ role: "user", content: "What is the capitol of {{state}}?" }], + messages: [{ role: "user", content: "What is the capital of {{country}}?" }], temperature: 0, }, }, @@ -44,7 +48,12 @@ const resp = await prisma.promptVariant.createMany({ sortIndex: 1, config: { model: "gpt-3.5-turbo", - messages: [{ role: "user", content: "What is the capitol of the US state {{state}}?" }], + messages: [ + { + role: "user", + content: "What is the capital of {{country}}? Only return the city name.", + }, + ], temperature: 0, }, }, @@ -61,7 +70,7 @@ await prisma.templateVariable.createMany({ data: [ { experimentId, - label: "state", + label: "country", }, ], }); @@ -78,21 +87,21 @@ await prisma.testScenario.createMany({ experimentId, sortIndex: 0, variableValues: { - state: "Washington", + country: "USA", }, }, { experimentId, sortIndex: 1, variableValues: { - state: "California", + country: "Spain", }, }, { experimentId, sortIndex: 2, variableValues: { - state: "Utah", + country: "Chile", }, }, ], diff --git a/src/components/OutputsTable/OutputCell.tsx b/src/components/OutputsTable/OutputCell.tsx index c05e3e0..f7567c8 100644 --- a/src/components/OutputsTable/OutputCell.tsx +++ b/src/components/OutputsTable/OutputCell.tsx @@ -2,7 +2,6 @@ import { api } from "~/utils/api"; import { PromptVariant, Scenario } from "./types"; import { Center, Spinner, Text } from "@chakra-ui/react"; import { useExperiment } from "~/utils/hooks"; -import { JSONSerializable } from "~/server/types"; import { cellPadding } from "../constants"; const CellShell = ({ children }: { children: React.ReactNode }) => ( @@ -19,12 +18,11 @@ export default function OutputCell({ variant: PromptVariant; }) { const experiment = useExperiment(); + const vars = api.templateVars.list.useQuery({ experimentId: experiment.data?.id ?? "" }).data; - const experimentVariables = experiment.data?.TemplateVariable.map((v) => v.label) ?? []; const scenarioVariables = scenario.variableValues as Record; const templateHasVariables = - experimentVariables.length === 0 || - experimentVariables.some((v) => scenarioVariables[v] !== undefined); + vars?.length === 0 || vars?.some((v) => scenarioVariables[v.label] !== undefined); let disabledReason: string | null = null; @@ -41,6 +39,8 @@ export default function OutputCell({ { enabled: disabledReason === null } ); + if (!vars) return; + if (disabledReason) return ( diff --git a/src/components/OutputsTable/ScenarioEditor.tsx b/src/components/OutputsTable/ScenarioEditor.tsx index 692f684..bc28ba1 100644 --- a/src/components/OutputsTable/ScenarioEditor.tsx +++ b/src/components/OutputsTable/ScenarioEditor.tsx @@ -25,8 +25,9 @@ export default function ScenarioEditor({ const [values, setValues] = useState>(savedValues); const experiment = useExperiment(); + const vars = api.templateVars.list.useQuery({ experimentId: experiment.data?.id ?? "" }).data; - const variableLabels = experiment.data?.TemplateVariable.map((v) => v.label) ?? []; + const variableLabels = vars?.map((v) => v.label) ?? []; const hasChanged = !isEqual(savedValues, values); @@ -113,58 +114,69 @@ export default function ScenarioEditor({ _hover={{ color: "gray.800", cursor: "pointer" }} /> - - {variableLabels.map((key) => { - const value = values[key] ?? ""; - const layoutDirection = value.length > 20 ? "column" : "row"; - return ( - - - {key} - -