lots of changes on making new experiments work

This commit is contained in:
Kyle Corbitt
2023-06-26 18:03:26 -07:00
parent dbc61b8672
commit 0646fc0ba3
17 changed files with 544 additions and 188 deletions

176
@types/nextjs-routes.d.ts vendored Normal file
View File

@@ -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: Pathname;
query?: Query | undefined;
hash?: string | null | undefined;
}
interface DynamicRoute<Pathname, Parameters> {
pathname: Pathname;
query: Parameters & Query;
hash?: string | null | undefined;
}
interface Query {
[key: string]: string | string[] | undefined;
};
export type RoutedQuery<P extends Route["pathname"]> = 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<NextGetServerSidePropsContext, 'params' | 'query' | 'defaultLocale' | 'locale' | 'locales'> & {
params: Extract<Route, { pathname: Pathname }>["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<Pathname, Preview>
) => Promise<NextGetServerSidePropsResult<Props>>
}
// 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<Route, { query: any }>["pathname"];
export interface LinkProps
extends Omit<NextLinkProps, "href" | "locale">,
AnchorHTMLAttributes<HTMLAnchorElement> {
href: Route | StaticRoute | Omit<Route, "pathname">
locale?: Locale | false;
}
type LinkReactElement = DetailedReactHTMLElement<
{
onMouseEnter?: MouseEventHandler<Element> | undefined;
onClick: MouseEventHandler;
href?: string | undefined;
ref?: any;
},
HTMLElement
>;
declare function Link(props: PropsWithChildren<LinkProps>): 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<Parameters<Router["push"]>[2]>;
type StaticRoute = Exclude<Route, { query: any }>["pathname"];
interface TransitionOptions extends Omit<NextTransitionOptions, "locale"> {
locale?: Locale | false;
}
type PathnameAndQuery<Pathname> = Required<
Pick<Extract<Route, { pathname: Pathname }>, "pathname" | "query">
>;
type AutomaticStaticOptimizedQuery<PaQ> = Omit<PaQ, "query"> & {
query: Partial<PaQ["query"]>;
};
type BaseRouter<PaQ> =
| ({ isReady: false } & AutomaticStaticOptimizedQuery<PaQ>)
| ({ isReady: true } & PaQ);
export type NextRouter<P extends Route["pathname"] = Route["pathname"]> =
BaseRouter<PathnameAndQuery<P>> &
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<Route, "pathname">,
as?: string,
options?: TransitionOptions
): Promise<boolean>;
replace(
url: Route | StaticRoute | Omit<Route, "pathname">,
as?: string,
options?: TransitionOptions
): Promise<boolean>;
route: P;
};
export function useRouter<P extends Route["pathname"]>(): NextRouter<P>;
}

View File

@@ -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);

View File

@@ -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",

68
pnpm-lock.yaml generated
View File

@@ -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==}

View File

@@ -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

View File

@@ -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",
},
},
],

View File

@@ -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<string, string>;
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 (
<CellShell>

View File

@@ -25,8 +25,9 @@ export default function ScenarioEditor({
const [values, setValues] = useState<Record<string, string>>(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,6 +114,9 @@ export default function ScenarioEditor({
_hover={{ color: "gray.800", cursor: "pointer" }}
/>
</Stack>
{variableLabels.length === 0 ? (
<Box color="gray.500">No scenario variables configured</Box>
) : (
<Stack>
{variableLabels.map((key) => {
const value = values[key] ?? "";
@@ -131,10 +135,17 @@ export default function ScenarioEditor({
px={2}
py={1}
placeholder="empty"
borderRadius="sm"
value={value}
onChange={(e) => {
setValues((prev) => ({ ...prev, [key]: e.target.value }));
}}
onKeyDown={(e) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
onSave();
}
}}
resize="none"
overflow="hidden"
minRows={1}
@@ -165,6 +176,7 @@ export default function ScenarioEditor({
</HStack>
)}
</Stack>
)}
</HStack>
);
}

View File

@@ -1,4 +1,4 @@
import { Box, Grid, GridItem, Heading, type SystemStyleObject } from "@chakra-ui/react";
import { Grid, GridItem, type SystemStyleObject } from "@chakra-ui/react";
import React, { useState } from "react";
import { api } from "~/utils/api";
import NewScenarioButton from "./NewScenarioButton";
@@ -8,7 +8,6 @@ import ScenarioEditor from "./ScenarioEditor";
import VariantConfigEditor from "./VariantConfigEditor";
import VariantHeader from "./VariantHeader";
import type { Scenario, PromptVariant } from "./types";
import { cellPadding } from "../constants";
import ScenarioHeader from "~/server/ScenarioHeader";
const stickyHeaderStyle: SystemStyleObject = {

View File

@@ -1,19 +0,0 @@
import { Box, Flex } from "@chakra-ui/react";
import Head from "next/head";
export default function AppNav(props: { children: React.ReactNode; title?: string }) {
return (
<Flex minH="100vh">
<Head>
<title>{props.title ? `${props.title} | Prompt Lab` : "Prompt Lab"}</title>
</Head>
{/* Placeholder for now */}
<Box bgColor="gray.100" flexShrink={0} width="200px">
Nav Sidebar
</Box>
<Box flex={1} overflowX="auto" overflowY="auto" h="100vh">
{props.children}
</Box>
</Flex>
);
}

View File

@@ -0,0 +1,67 @@
import { Box, Flex, Stack, Button, Heading, VStack, Icon, HStack } from "@chakra-ui/react";
import Head from "next/head";
import { api } from "~/utils/api";
import { BsPlusSquare } from "react-icons/bs";
import { RiFlaskLine } from "react-icons/ri";
import { useRouter } from "next/router";
import Link from "next/link";
import { useHandledAsyncCallback } from "~/utils/hooks";
const NavButton = ({ label, onClick }) => {
return (
<Button
variant="ghost"
justifyContent="start"
onClick={onClick}
w="full"
leftIcon={<BsPlusSquare />}
>
{label}
</Button>
);
};
export default function AppShell(props: { children: React.ReactNode; title?: string }) {
const experiments = api.experiments.list.useQuery();
const router = useRouter();
const utils = api.useContext();
const createMutation = api.experiments.create.useMutation();
const [createExperiment] = useHandledAsyncCallback(async () => {
const newExperiment = await createMutation.mutateAsync({ label: "New Experiment" });
await utils.experiments.list.invalidate();
await router.push({ pathname: "/experiments/[id]", query: { id: newExperiment.id } });
}, [createMutation, router]);
return (
<Flex minH="100vh">
<Head>
<title>{props.title ? `${props.title} | Prompt Lab` : "Prompt Lab"}</title>
</Head>
<Box bgColor="gray.100" flexShrink={0} width="220px" p={8}>
<VStack align="start" spacing={4}>
<HStack align="center" spacing={2}>
<Icon as={RiFlaskLine} boxSize={6} />
<Heading size="sm" textAlign="center">
Experiments
</Heading>
</HStack>
{experiments?.data?.map((exp) => (
<Box
key={exp.id}
as={Link}
href={{ pathname: "/experiments/[id]", query: { id: exp.id } }}
justifyContent="start"
>
{exp.label}
</Box>
))}
<NavButton label="New Experiment" onClick={createExperiment} />
</VStack>
</Box>
<Box flex={1} overflowX="auto" overflowY="auto" h="100vh">
{props.children}
</Box>
</Flex>
);
}

View File

@@ -1,7 +1,7 @@
import { Box, Center } from "@chakra-ui/react";
import { useRouter } from "next/router";
import OutputsTable from "~/components/OutputsTable";
import AppNav from "~/components/nav/AppNav";
import AppShell from "~/components/nav/AppShell";
import { api } from "~/utils/api";
export default function Experiment() {
@@ -14,19 +14,19 @@ export default function Experiment() {
if (!experiment.isLoading && !experiment.data) {
return (
<AppNav title="Experiment not found">
<AppShell title="Experiment not found">
<Center h="100vh">
<div>Experiment not found 😕</div>
</Center>
</AppNav>
</AppShell>
);
}
return (
<AppNav title={experiment.data?.label}>
<AppShell title={experiment.data?.label}>
<Box minH="100vh" mb={50}>
<OutputsTable experimentId={router.query.id as string | undefined} />
</Box>
</AppNav>
</AppShell>
);
}

View File

@@ -1,63 +1,62 @@
import { Text, Box, Button, HStack, Heading, Icon, Input, Stack, Code } from "@chakra-ui/react";
import { useState } from "react";
import { BsCheck, BsChevronDown, BsX } from "react-icons/bs";
import { BsCheck, BsChevronDown, BsChevronUp, BsX } from "react-icons/bs";
import { cellPadding } from "~/components/constants";
import { api } from "~/utils/api";
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
export default function ScenarioHeader() {
const experiment = useExperiment();
const vars =
api.templateVars.list.useQuery({ experimentId: experiment.data?.id ?? "" }).data ?? [];
const initialVariables = experiment.data?.TemplateVariable ?? [];
const [variables, setVariables] = useState<string[]>(initialVariables.map((v) => v.label));
const [newVariable, setNewVariable] = useState<string>("");
const newVarIsValid = newVariable.length > 0 && !vars.map((v) => v.label).includes(newVariable);
const [editing, setEditing] = useState(false);
const utils = api.useContext();
const setVarsMutation = api.experiments.setTemplateVariables.useMutation();
const [onSave] = useHandledAsyncCallback(async () => {
const addVarMutation = api.templateVars.create.useMutation();
const [onAddVar] = useHandledAsyncCallback(async () => {
if (!experiment.data?.id) return;
setEditing(false);
await setVarsMutation.mutateAsync({
id: experiment.data.id,
labels: variables,
if (!newVarIsValid) return;
await addVarMutation.mutateAsync({
experimentId: experiment.data.id,
label: newVariable,
});
await utils.experiments.get.invalidate();
}, [setVarsMutation, experiment.data?.id, variables]);
await utils.templateVars.list.invalidate();
setNewVariable("");
}, [addVarMutation, experiment.data?.id, newVarIsValid, newVariable]);
const deleteMutation = api.templateVars.delete.useMutation();
const [onDeleteVar] = useHandledAsyncCallback(async (id: string) => {
await deleteMutation.mutateAsync({ id });
await utils.templateVars.list.invalidate();
}, []);
return (
<Stack flex={1} px={cellPadding.x} py={cellPadding.y}>
<HStack>
<Heading size="sm" fontWeight="bold" flex={1}>
Scenario
Scenario Vars
</Heading>
{editing ? (
<HStack>
<Button size="xs" colorScheme="gray" onClick={() => setEditing(false)}>
Cancel
</Button>
<Button size="xs" colorScheme="blue" onClick={onSave}>
Save
</Button>
</HStack>
) : (
{
<Button
size="xs"
variant="outline"
colorScheme="blue"
rightIcon={<Icon as={BsChevronDown} />}
onClick={() => setEditing(true)}
rightIcon={<Icon as={editing ? BsCheck : BsChevronDown} />}
onClick={() => setEditing((editing) => !editing)}
>
Configure
{editing ? "Finish" : "Configure"}
</Button>
)}
}
</HStack>
{editing && (
<Stack spacing={2} pt={2}>
<Text fontSize="sm">
You can use variables in your prompt text inside <Code>{`{{curly_braces}}`}</Code>.
Define scenario variables. Reference them from your prompt variants using{" "}
<Code>{`{{curly_braces}}`}</Code>.
</Text>
<HStack spacing={0}>
<Input
@@ -67,25 +66,28 @@ export default function ScenarioHeader() {
borderRightRadius={0}
value={newVariable}
onChange={(e) => setNewVariable(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
onAddVar();
}
}}
/>
<Button
size="xs"
height="100%"
borderLeftRadius={0}
isDisabled={newVariable.length === 0 || variables.includes(newVariable)}
onClick={() => {
setVariables([...variables, newVariable]);
setNewVariable("");
}}
isDisabled={!newVarIsValid}
onClick={onAddVar}
>
<Icon as={BsCheck} boxSize={8} />
</Button>
</HStack>
<HStack spacing={2} py={4} wrap="wrap">
{variables.map((label, i) => (
{vars.map((variable, i) => (
<HStack
key={label}
key={variable.id}
spacing={0}
bgColor="blue.100"
color="blue.600"
@@ -94,7 +96,7 @@ export default function ScenarioHeader() {
fontWeight="bold"
>
<Text fontSize="sm" flex={1}>
{label}
{variable.label}
</Text>
<Button
size="xs"
@@ -103,11 +105,7 @@ export default function ScenarioHeader() {
p="unset"
minW="unset"
px="unset"
onClick={() => {
const newVariables = [...variables];
newVariables.splice(i, 1);
setVariables(newVariables);
}}
onClick={() => onDeleteVar(variable.id)}
>
<Icon as={BsX} boxSize={6} color="blue.800" />
</Button>

View File

@@ -3,6 +3,7 @@ import { createTRPCRouter } from "~/server/api/trpc";
import { experimentsRouter } from "./routers/experiments.router";
import { scenariosRouter } from "./routers/scenarios.router";
import { modelOutputsRouter } from "./routers/modelOutputs.router";
import { templateVarsRouter } from "./routers/templateVariables.router";
/**
* This is the primary router for your server.
@@ -14,6 +15,7 @@ export const appRouter = createTRPCRouter({
experiments: experimentsRouter,
scenarios: scenariosRouter,
outputs: modelOutputsRouter,
templateVars: templateVarsRouter,
});
// export type definition of API

View File

@@ -3,50 +3,64 @@ import { createTRPCRouter, publicProcedure, protectedProcedure } from "~/server/
import { prisma } from "~/server/db";
export const experimentsRouter = createTRPCRouter({
list: publicProcedure.query(async () => {
return await prisma.experiment.findMany({
orderBy: {
sortIndex: "asc",
},
});
}),
get: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input }) => {
return await prisma.experiment.findFirst({
where: {
id: input.id,
},
include: {
TemplateVariable: {
orderBy: {
createdAt: "asc",
},
select: {
id: true,
label: true,
},
},
},
});
}),
setTemplateVariables: publicProcedure
.input(
z.object({
id: z.string(),
labels: z.array(z.string()),
create: publicProcedure.input(z.object({})).mutation(async () => {
const maxSortIndex =
(
await prisma.experiment.aggregate({
_max: {
sortIndex: true,
},
})
)
.mutation(async ({ input }) => {
const existing = await prisma.templateVariable.findMany({
where: { experimentId: input.id },
});
const toDelete = existing.filter((e) => !input.labels.includes(e.label));
)._max?.sortIndex ?? 0;
const toCreate = new Set(
input.labels.filter((l) => !existing.map((e) => e.label).includes(l))
).values();
const exp = await prisma.experiment.create({
data: {
sortIndex: maxSortIndex + 1,
label: `Experiment ${maxSortIndex + 1}`,
},
});
await prisma.$transaction([
prisma.templateVariable.deleteMany({
where: { id: { in: toDelete.map((e) => e.id) } },
prisma.promptVariant.create({
data: {
experimentId: exp.id,
label: "Prompt Variant 1",
sortIndex: 0,
config: {
model: "gpt-3.5-turbo",
messages: [
{
role: "system",
content: "count to three in Spanish...",
},
],
},
},
}),
prisma.templateVariable.createMany({
data: [...toCreate].map((l) => ({ label: l, experimentId: input.id })),
prisma.testScenario.create({
data: {
experimentId: exp.id,
variableValues: {},
},
}),
]);
return null;
return exp;
}),
});

View File

@@ -0,0 +1,35 @@
import { z } from "zod";
import { createTRPCRouter, publicProcedure, protectedProcedure } from "~/server/api/trpc";
import { prisma } from "~/server/db";
export const templateVarsRouter = createTRPCRouter({
create: publicProcedure
.input(z.object({ experimentId: z.string(), label: z.string() }))
.mutation(async ({ input }) => {
await prisma.templateVariable.create({
data: {
experimentId: input.experimentId,
label: input.label,
},
});
}),
delete: publicProcedure.input(z.object({ id: z.string() })).mutation(async ({ input }) => {
await prisma.templateVariable.delete({ where: { id: input.id } });
}),
list: publicProcedure.input(z.object({ experimentId: z.string() })).query(async ({ input }) => {
return await prisma.templateVariable.findMany({
where: {
experimentId: input.experimentId,
},
orderBy: {
createdAt: "asc",
},
select: {
id: true,
label: true,
},
});
}),
});

View File

@@ -16,13 +16,19 @@ const theme = extendTheme({
},
},
Input: {
sizes: {
md: {
baseStyle: {
field: {
borderRadius: "sm",
},
},
},
Textarea: {
baseStyle: {
borderRadius: "sm",
field: {
borderRadius: "sm",
},
},
},
},
});