can add scenarios and it mostly works

This commit is contained in:
Kyle Corbitt
2023-06-23 20:00:46 -07:00
parent c497b74208
commit 8534477236
13 changed files with 278 additions and 68 deletions

View File

@@ -16,6 +16,8 @@
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/server": "^11.11.0", "@emotion/server": "^11.11.0",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@fontsource/poppins": "^5.0.3",
"@fontsource/roboto": "^5.0.3",
"@monaco-editor/react": "^4.5.1", "@monaco-editor/react": "^4.5.1",
"@next-auth/prisma-adapter": "^1.0.5", "@next-auth/prisma-adapter": "^1.0.5",
"@prisma/client": "^4.14.0", "@prisma/client": "^4.14.0",
@@ -34,6 +36,8 @@
"next-auth": "^4.22.1", "next-auth": "^4.22.1",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-icons": "^4.10.1",
"react-textarea-autosize": "^8.5.0",
"superjson": "1.12.2", "superjson": "1.12.2",
"tsx": "^3.12.7", "tsx": "^3.12.7",
"zod": "^3.21.4" "zod": "^3.21.4"

77
pnpm-lock.yaml generated
View File

@@ -20,6 +20,12 @@ dependencies:
'@emotion/styled': '@emotion/styled':
specifier: ^11.11.0 specifier: ^11.11.0
version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.6)(react@18.2.0) version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.6)(react@18.2.0)
'@fontsource/poppins':
specifier: ^5.0.3
version: 5.0.3
'@fontsource/roboto':
specifier: ^5.0.3
version: 5.0.3
'@monaco-editor/react': '@monaco-editor/react':
specifier: ^4.5.1 specifier: ^4.5.1
version: 4.5.1(monaco-editor@0.39.0)(react-dom@18.2.0)(react@18.2.0) version: 4.5.1(monaco-editor@0.39.0)(react-dom@18.2.0)(react@18.2.0)
@@ -74,6 +80,12 @@ dependencies:
react-dom: react-dom:
specifier: 18.2.0 specifier: 18.2.0
version: 18.2.0(react@18.2.0) version: 18.2.0(react@18.2.0)
react-icons:
specifier: ^4.10.1
version: 4.10.1(react@18.2.0)
react-textarea-autosize:
specifier: ^8.5.0
version: 8.5.0(@types/react@18.2.6)(react@18.2.0)
superjson: superjson:
specifier: 1.12.2 specifier: 1.12.2
version: 1.12.2 version: 1.12.2
@@ -1670,6 +1682,14 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true dev: true
/@fontsource/poppins@5.0.3:
resolution: {integrity: sha512-5lL2vmvmh4zbknTh1nVH9pTWyhYqAlYMHIAnkNhqo5qwMZGlr+coM1dtMwiQHRBgmHAl3ZvJ35Bj0s8cpmXZbg==}
dev: false
/@fontsource/roboto@5.0.3:
resolution: {integrity: sha512-jbZDFwEFARDlo8TqG7th/xjhuq87GYfFpFb+uxuy+0Ng1bhRVgBRWlLj8+WIKhCTOr+h4QXbjpybLWFLUirOwQ==}
dev: false
/@humanwhocodes/config-array@0.11.10: /@humanwhocodes/config-array@0.11.10:
resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
engines: {node: '>=10.10.0'} engines: {node: '>=10.10.0'}
@@ -4135,6 +4155,14 @@ packages:
use-sidecar: 1.1.2(@types/react@18.2.6)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.2.6)(react@18.2.0)
dev: false dev: false
/react-icons@4.10.1(react@18.2.0):
resolution: {integrity: sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==}
peerDependencies:
react: '*'
dependencies:
react: 18.2.0
dev: false
/react-is@16.13.1: /react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -4198,6 +4226,20 @@ packages:
tslib: 2.5.3 tslib: 2.5.3
dev: false dev: false
/react-textarea-autosize@8.5.0(@types/react@18.2.6)(react@18.2.0):
resolution: {integrity: sha512-cp488su3U9RygmHmGpJp0KEt0i/+57KCK33XVPH+50swVRBhIZYh0fGduz2YLKXwl9vSKBZ9HUXcg9PQXUXqIw==}
engines: {node: '>=10'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@babel/runtime': 7.22.5
react: 18.2.0
use-composed-ref: 1.3.0(react@18.2.0)
use-latest: 1.2.1(@types/react@18.2.6)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
/react@18.2.0: /react@18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -4658,6 +4700,41 @@ packages:
tslib: 2.5.3 tslib: 2.5.3
dev: false dev: false
/use-composed-ref@1.3.0(react@18.2.0):
resolution: {integrity: sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/use-isomorphic-layout-effect@1.1.2(@types/react@18.2.6)(react@18.2.0):
resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.2.6
react: 18.2.0
dev: false
/use-latest@1.2.1(@types/react@18.2.6)(react@18.2.0):
resolution: {integrity: sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.2.6
react: 18.2.0
use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.6)(react@18.2.0)
dev: false
/use-sidecar@1.1.2(@types/react@18.2.6)(react@18.2.0): /use-sidecar@1.1.2(@types/react@18.2.6)(react@18.2.0):
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
engines: {node: '>=10'} engines: {node: '>=10'}

View File

@@ -26,7 +26,12 @@ export default function EditableVariantLabel(props: { variant: PromptVariant })
ref={labelRef} ref={labelRef}
contentEditable contentEditable
suppressContentEditableWarning suppressContentEditableWarning
borderWidth={1}
borderColor="transparent"
_hover={{ borderColor: "gray.300" }}
_focus={{ borderColor: "blue.500", outline: "none" }}
onBlur={onBlur} onBlur={onBlur}
py={2}
> >
{props.variant.label} {props.variant.label}
</Heading> </Heading>

View File

@@ -0,0 +1,34 @@
import { Button } from "@chakra-ui/react";
import { BsPlus } from "react-icons/bs";
import { api } from "~/utils/api";
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
export default function NewScenarioButton() {
const experiment = useExperiment();
const mutation = api.scenarios.create.useMutation();
const utils = api.useContext();
const [onClick] = useHandledAsyncCallback(async () => {
await mutation.mutateAsync({
experimentId: experiment.data!.id,
});
await utils.scenarios.list.invalidate();
}, [mutation]);
return (
<Button
w="100%"
borderRadius={0}
alignItems="center"
justifyContent="flex-start"
fontWeight="normal"
bgColor="transparent"
_hover={{ bgColor: "gray.100" }}
px={2}
onClick={onClick}
>
<BsPlus size={24} />
New Scenario
</Button>
);
}

View File

@@ -1,6 +1,7 @@
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { PromptVariant, Scenario } from "./types"; import { PromptVariant, Scenario } from "./types";
import { Center } from "@chakra-ui/react"; import { Center, Text } from "@chakra-ui/react";
import { useExperiment } from "~/utils/hooks";
export default function OutputCell({ export default function OutputCell({
scenario, scenario,
@@ -9,12 +10,32 @@ export default function OutputCell({
scenario: Scenario; scenario: Scenario;
variant: PromptVariant; variant: PromptVariant;
}) { }) {
const output = api.outputs.get.useQuery({ const experiment = useExperiment();
scenarioId: scenario.id,
variantId: variant.id,
});
if (!output.data) return null; 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);
const output = api.outputs.get.useQuery(
{
scenarioId: scenario.id,
variantId: variant.id,
},
{ enabled: templateHasVariables }
);
if (!templateHasVariables)
return (
<Center h="100%">
<Text color="gray.500">Add a scenario variable to see output</Text>
</Center>
);
if (output.isLoading) return <Center h="100%">Loading...</Center>;
if (!output.data) return <Center h="100%">No output</Center>;
return ( return (
<Center h="100%"> <Center h="100%">

View File

@@ -1,9 +1,11 @@
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { isEqual } from "lodash"; import { isEqual } from "lodash";
import { PromptVariant, Scenario } from "./types"; import { type Scenario } from "./types";
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks"; import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
import { useState } from "react"; import { useState } from "react";
import { Badge, Button, Flex, HStack, Stack, Textarea } from "@chakra-ui/react"; import ResizeTextarea from "react-textarea-autosize";
import { Box, Button, Flex, HStack, Stack, Textarea } from "@chakra-ui/react";
export default function ScenarioHeader({ scenario }: { scenario: Scenario }) { export default function ScenarioHeader({ scenario }: { scenario: Scenario }) {
const savedValues = scenario.variableValues as Record<string, string>; const savedValues = scenario.variableValues as Record<string, string>;
@@ -30,34 +32,53 @@ export default function ScenarioHeader({ scenario }: { scenario: Scenario }) {
return ( return (
<Stack> <Stack>
{variableLabels.map((key) => { {variableLabels.map((key) => {
const value = values[key] ?? "";
const layoutDirection = value.length > 20 ? "column" : "row";
return ( return (
<Flex key={key}> <Flex
<Badge>{key}</Badge> key={key}
direction={layoutDirection}
alignItems={layoutDirection === "column" ? "flex-start" : "center"}
flexWrap="wrap"
>
<Box bgColor="blue.100" color="blue.600" px={2} fontSize="xs" fontWeight="bold">
{key}
</Box>
<Textarea <Textarea
key={key} borderRadius={0}
value={values[key] ?? ""} px={2}
py={1}
placeholder="empty"
value={value}
onChange={(e) => { onChange={(e) => {
setValues((prev) => ({ ...prev, [key]: e.target.value })); setValues((prev) => ({ ...prev, [key]: e.target.value }));
}} }}
rows={1} resize="none"
// TODO: autosize overflow="hidden"
maxRows={20} minRows={1}
minH="unset"
as={ResizeTextarea}
flex={layoutDirection === "row" ? 1 : undefined}
borderColor={hasChanged ? "blue.300" : "transparent"}
_hover={{ borderColor: "gray.300" }}
_focus={{ borderColor: "blue.500", outline: "none" }}
/> />
</Flex> </Flex>
); );
})} })}
{hasChanged && ( {hasChanged && (
<HStack spacing={4}> <HStack justify="right">
<Button <Button
size="xs" size="sm"
onClick={() => { borderRadius={0}
onMouseDown={() => {
setValues(savedValues); setValues(savedValues);
}} }}
color="gray" colorScheme="gray"
> >
Reset Reset
</Button> </Button>
<Button size="xs" onClick={onSave}> <Button size="sm" borderRadius={0} onMouseDown={onSave} colorScheme="blue">
Save Save
</Button> </Button>
</HStack> </HStack>

View File

@@ -108,19 +108,20 @@ export default function VariantConfigEditor(props: {
<Box w="100%" pos="relative"> <Box w="100%" pos="relative">
<div id={editorId} style={{ height: "300px", width: "100%" }}></div> <div id={editorId} style={{ height: "300px", width: "100%" }}></div>
{isChanged && ( {isChanged && (
<HStack pos="absolute" bottom={0} right={0} spacing={4}> <HStack pos="absolute" bottom={0} right={0}>
<Button <Button
colorScheme="gray" colorScheme="gray"
size="xs" size="sm"
onClick={() => { onClick={() => {
editorRef.current?.setValue(props.savedConfig); editorRef.current?.setValue(props.savedConfig);
checkForChanges(); checkForChanges();
}} }}
borderRadius={0}
> >
Reset Reset
</Button> </Button>
<Tooltip label={`${modifierKey} + Enter`}> <Tooltip label={`${modifierKey} + Enter`}>
<Button size="xs" onClick={onSave} colorScheme="blue"> <Button size="sm" onClick={onSave} colorScheme="blue" borderRadius={0}>
Save Save
</Button> </Button>
</Tooltip> </Tooltip>

View File

@@ -21,17 +21,15 @@ export default function VariantHeader({ variant }: { variant: PromptVariant }) {
title: "Invalid JSON", title: "Invalid JSON",
description: "Please fix the JSON before saving.", description: "Please fix the JSON before saving.",
status: "error", status: "error",
duration: 5000,
position: "top",
}); });
return; return;
} }
if (parsedConfig === null) { if (parsedConfig === null) {
notifications.show({ toast({
title: "Invalid JSON", title: "Invalid JSON",
message: "Please fix the JSON before saving.", description: "Please fix the JSON before saving.",
color: "red", status: "error",
}); });
return; return;
} }
@@ -43,7 +41,7 @@ export default function VariantHeader({ variant }: { variant: PromptVariant }) {
await utils.promptVariants.list.invalidate(); await utils.promptVariants.list.invalidate();
}, },
[variant.id, replaceWithConfig, utils.promptVariants.list] [variant.id, replaceWithConfig, utils.promptVariants.list, toast]
); );
return ( return (

View File

@@ -1,14 +1,11 @@
import { useMemo } from "react";
import { RouterOutputs, api } from "~/utils/api"; import { RouterOutputs, api } from "~/utils/api";
import { type PromptVariant } from "./types"; import { type PromptVariant } from "./types";
import VariantHeader from "./VariantHeader"; import VariantHeader from "./VariantHeader";
import OutputCell from "./OutputCell"; import OutputCell from "./OutputCell";
import ScenarioHeader from "./ScenarioHeader"; import ScenarioHeader from "./ScenarioHeader";
import React from "react"; import React from "react";
import { Box, Heading } from "@chakra-ui/react"; import { Box, Grid, GridItem, Heading } from "@chakra-ui/react";
import NewScenarioButton from "./NewScenarioButton";
const cellPaddingX = 4;
const cellPaddingY = 2;
export default function OutputsTable({ experimentId }: { experimentId: string | undefined }) { export default function OutputsTable({ experimentId }: { experimentId: string | undefined }) {
const variants = api.promptVariants.list.useQuery( const variants = api.promptVariants.list.useQuery(
@@ -24,37 +21,48 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
if (!variants.data || !scenarios.data) return null; if (!variants.data || !scenarios.data) return null;
return ( return (
<Box p={4}> <Grid
<div p={4}
style={{ display="grid"
display: "grid", gridTemplateColumns={`200px repeat(${variants.data.length}, minmax(300px, 1fr))`}
gridTemplateColumns: `200px repeat(${variants.data.length}, minmax(300px, 1fr))`, overflowX="auto"
overflowX: "auto", sx={{
}} "> *": {
> borderColor: "gray.300",
<Box px={cellPaddingX} py={cellPaddingY} display="flex" sx={{}}> borderBottomWidth: 1,
<Heading size="md" fontWeight="bold"> paddingX: 4,
Scenario paddingY: 2,
</Heading> },
</Box> "> *:last-child": {
{variants.data.map((variant) => ( borderRightWidth: 0,
<Box key={variant.uiId} px={cellPaddingX} py={cellPaddingY}> },
<VariantHeader key={variant.uiId} variant={variant} /> }}
</Box> >
))} <GridItem display="flex" alignItems="flex-end">
{scenarios.data.map((scenario) => ( <Heading size="md" fontWeight="bold">
<React.Fragment key={scenario.uiId}> Scenario
<Box px={cellPaddingX} py={cellPaddingY}> </Heading>
<ScenarioHeader scenario={scenario} /> </GridItem>
</Box> {variants.data.map((variant) => (
{variants.data.map((variant) => ( <GridItem key={variant.uiId}>
<Box key={variant.id} px={cellPaddingX} py={cellPaddingY}> <VariantHeader key={variant.uiId} variant={variant} />
<OutputCell key={variant.id} scenario={scenario} variant={variant} /> </GridItem>
</Box> ))}
))} {scenarios.data.map((scenario) => (
</React.Fragment> <React.Fragment key={scenario.uiId}>
))} <GridItem>
</div> <ScenarioHeader scenario={scenario} />
</Box> </GridItem>
{variants.data.map((variant) => (
<GridItem key={variant.id}>
<OutputCell key={variant.id} scenario={scenario} variant={variant} />
</GridItem>
))}
</React.Fragment>
))}
<GridItem borderBottomWidth={0} w="100%" colSpan={variants.data.length + 1} px={0} py={0}>
<NewScenarioButton />
</GridItem>
</Grid>
); );
} }

View File

@@ -3,6 +3,7 @@ import { SessionProvider } from "next-auth/react";
import { type AppType } from "next/app"; import { type AppType } from "next/app";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { ChakraProvider } from "@chakra-ui/react"; import { ChakraProvider } from "@chakra-ui/react";
import theme from "~/utils/theme";
const MyApp: AppType<{ session: Session | null }> = ({ const MyApp: AppType<{ session: Session | null }> = ({
Component, Component,
@@ -10,7 +11,7 @@ const MyApp: AppType<{ session: Session | null }> = ({
}) => { }) => {
return ( return (
<SessionProvider session={session}> <SessionProvider session={session}>
<ChakraProvider> <ChakraProvider theme={theme}>
<Component {...pageProps} /> <Component {...pageProps} />
</ChakraProvider> </ChakraProvider>
</SessionProvider> </SessionProvider>

View File

@@ -24,7 +24,7 @@ export default function Experiment() {
return ( return (
<AppNav title={experiment.data?.label}> <AppNav title={experiment.data?.label}>
<Box sx={{ minHeight: "100vh" }}> <Box minH="100vh" mb={50}>
<OutputsTable experimentId={router.query.id as string | undefined} /> <OutputsTable experimentId={router.query.id as string | undefined} />
</Box> </Box>
</AppNav> </AppNav>

View File

@@ -15,6 +15,34 @@ export const scenariosRouter = createTRPCRouter({
}); });
}), }),
create: publicProcedure
.input(
z.object({
experimentId: z.string(),
})
)
.mutation(async ({ input }) => {
const maxSortIndex =
(
await prisma.testScenario.aggregate({
where: {
experimentId: input.experimentId,
},
_max: {
sortIndex: true,
},
})
)._max.sortIndex ?? 0;
const newScenario = await prisma.testScenario.create({
data: {
experimentId: input.experimentId,
sortIndex: maxSortIndex + 1,
variableValues: {},
},
});
}),
replaceWithValues: publicProcedure replaceWithValues: publicProcedure
.input( .input(
z.object({ z.object({

12
src/utils/theme.ts Normal file
View File

@@ -0,0 +1,12 @@
import { extendTheme } from "@chakra-ui/react";
import "@fontsource/poppins";
import "@fontsource/roboto";
const theme = extendTheme({
fonts: {
heading: "Poppins, sans-serif",
body: "Roboto, sans-serif",
},
});
export default theme;