diff --git a/package.json b/package.json index f8dd22f..faf785f 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "@emotion/react": "^11.11.1", "@emotion/server": "^11.11.0", "@emotion/styled": "^11.11.0", + "@fontsource/poppins": "^5.0.3", + "@fontsource/roboto": "^5.0.3", "@monaco-editor/react": "^4.5.1", "@next-auth/prisma-adapter": "^1.0.5", "@prisma/client": "^4.14.0", @@ -34,6 +36,8 @@ "next-auth": "^4.22.1", "react": "18.2.0", "react-dom": "18.2.0", + "react-icons": "^4.10.1", + "react-textarea-autosize": "^8.5.0", "superjson": "1.12.2", "tsx": "^3.12.7", "zod": "^3.21.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03d7926..1eee158 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,12 @@ dependencies: '@emotion/styled': specifier: ^11.11.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': specifier: ^4.5.1 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: specifier: 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: specifier: 1.12.2 version: 1.12.2 @@ -1670,6 +1682,14 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 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: resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} engines: {node: '>=10.10.0'} @@ -4135,6 +4155,14 @@ packages: use-sidecar: 1.1.2(@types/react@18.2.6)(react@18.2.0) 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: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -4198,6 +4226,20 @@ packages: tslib: 2.5.3 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: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -4658,6 +4700,41 @@ packages: tslib: 2.5.3 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): resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} diff --git a/src/components/OutputsTable/EditableVariantLabel.tsx b/src/components/OutputsTable/EditableVariantLabel.tsx index 5188678..b4db3d3 100644 --- a/src/components/OutputsTable/EditableVariantLabel.tsx +++ b/src/components/OutputsTable/EditableVariantLabel.tsx @@ -26,7 +26,12 @@ export default function EditableVariantLabel(props: { variant: PromptVariant }) ref={labelRef} contentEditable suppressContentEditableWarning + borderWidth={1} + borderColor="transparent" + _hover={{ borderColor: "gray.300" }} + _focus={{ borderColor: "blue.500", outline: "none" }} onBlur={onBlur} + py={2} > {props.variant.label} diff --git a/src/components/OutputsTable/NewScenarioButton.tsx b/src/components/OutputsTable/NewScenarioButton.tsx new file mode 100644 index 0000000..68801cb --- /dev/null +++ b/src/components/OutputsTable/NewScenarioButton.tsx @@ -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 ( + + + New Scenario + + ); +} diff --git a/src/components/OutputsTable/OutputCell.tsx b/src/components/OutputsTable/OutputCell.tsx index 3e1858e..a112363 100644 --- a/src/components/OutputsTable/OutputCell.tsx +++ b/src/components/OutputsTable/OutputCell.tsx @@ -1,6 +1,7 @@ import { api } from "~/utils/api"; 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({ scenario, @@ -9,12 +10,32 @@ export default function OutputCell({ scenario: Scenario; variant: PromptVariant; }) { - const output = api.outputs.get.useQuery({ - scenarioId: scenario.id, - variantId: variant.id, - }); + const experiment = useExperiment(); - if (!output.data) return null; + 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); + + const output = api.outputs.get.useQuery( + { + scenarioId: scenario.id, + variantId: variant.id, + }, + { enabled: templateHasVariables } + ); + + if (!templateHasVariables) + return ( + + Add a scenario variable to see output + + ); + + if (output.isLoading) return Loading...; + + if (!output.data) return No output; return ( diff --git a/src/components/OutputsTable/ScenarioHeader.tsx b/src/components/OutputsTable/ScenarioHeader.tsx index efa4053..e2cca13 100644 --- a/src/components/OutputsTable/ScenarioHeader.tsx +++ b/src/components/OutputsTable/ScenarioHeader.tsx @@ -1,9 +1,11 @@ import { api } from "~/utils/api"; import { isEqual } from "lodash"; -import { PromptVariant, Scenario } from "./types"; +import { type Scenario } from "./types"; import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks"; 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 }) { const savedValues = scenario.variableValues as Record; @@ -30,34 +32,53 @@ export default function ScenarioHeader({ scenario }: { scenario: Scenario }) { return ( {variableLabels.map((key) => { + const value = values[key] ?? ""; + const layoutDirection = value.length > 20 ? "column" : "row"; return ( - - {key} + + + {key} + { setValues((prev) => ({ ...prev, [key]: e.target.value })); }} - rows={1} - // TODO: autosize - maxRows={20} + resize="none" + overflow="hidden" + 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" }} /> ); })} {hasChanged && ( - + { + size="sm" + borderRadius={0} + onMouseDown={() => { setValues(savedValues); }} - color="gray" + colorScheme="gray" > Reset - + Save diff --git a/src/components/OutputsTable/VariantConfigEditor.tsx b/src/components/OutputsTable/VariantConfigEditor.tsx index e1c95db..fe21eff 100644 --- a/src/components/OutputsTable/VariantConfigEditor.tsx +++ b/src/components/OutputsTable/VariantConfigEditor.tsx @@ -108,19 +108,20 @@ export default function VariantConfigEditor(props: { {isChanged && ( - + { editorRef.current?.setValue(props.savedConfig); checkForChanges(); }} + borderRadius={0} > Reset - + Save diff --git a/src/components/OutputsTable/VariantHeader.tsx b/src/components/OutputsTable/VariantHeader.tsx index 97c707c..c45aefb 100644 --- a/src/components/OutputsTable/VariantHeader.tsx +++ b/src/components/OutputsTable/VariantHeader.tsx @@ -21,17 +21,15 @@ export default function VariantHeader({ variant }: { variant: PromptVariant }) { title: "Invalid JSON", description: "Please fix the JSON before saving.", status: "error", - duration: 5000, - position: "top", }); return; } if (parsedConfig === null) { - notifications.show({ + toast({ title: "Invalid JSON", - message: "Please fix the JSON before saving.", - color: "red", + description: "Please fix the JSON before saving.", + status: "error", }); return; } @@ -43,7 +41,7 @@ export default function VariantHeader({ variant }: { variant: PromptVariant }) { await utils.promptVariants.list.invalidate(); }, - [variant.id, replaceWithConfig, utils.promptVariants.list] + [variant.id, replaceWithConfig, utils.promptVariants.list, toast] ); return ( diff --git a/src/components/OutputsTable/index.tsx b/src/components/OutputsTable/index.tsx index e0c795e..a0753be 100644 --- a/src/components/OutputsTable/index.tsx +++ b/src/components/OutputsTable/index.tsx @@ -1,14 +1,11 @@ -import { useMemo } from "react"; import { RouterOutputs, api } from "~/utils/api"; import { type PromptVariant } from "./types"; import VariantHeader from "./VariantHeader"; import OutputCell from "./OutputCell"; import ScenarioHeader from "./ScenarioHeader"; import React from "react"; -import { Box, Heading } from "@chakra-ui/react"; - -const cellPaddingX = 4; -const cellPaddingY = 2; +import { Box, Grid, GridItem, Heading } from "@chakra-ui/react"; +import NewScenarioButton from "./NewScenarioButton"; export default function OutputsTable({ experimentId }: { experimentId: string | undefined }) { const variants = api.promptVariants.list.useQuery( @@ -24,37 +21,48 @@ export default function OutputsTable({ experimentId }: { experimentId: string | if (!variants.data || !scenarios.data) return null; return ( - - - - - Scenario - - - {variants.data.map((variant) => ( - - - - ))} - {scenarios.data.map((scenario) => ( - - - - - {variants.data.map((variant) => ( - - - - ))} - - ))} - - + *": { + borderColor: "gray.300", + borderBottomWidth: 1, + paddingX: 4, + paddingY: 2, + }, + "> *:last-child": { + borderRightWidth: 0, + }, + }} + > + + + Scenario + + + {variants.data.map((variant) => ( + + + + ))} + {scenarios.data.map((scenario) => ( + + + + + {variants.data.map((variant) => ( + + + + ))} + + ))} + + + + ); } diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 353f087..e7d52e5 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -3,6 +3,7 @@ import { SessionProvider } from "next-auth/react"; import { type AppType } from "next/app"; import { api } from "~/utils/api"; import { ChakraProvider } from "@chakra-ui/react"; +import theme from "~/utils/theme"; const MyApp: AppType<{ session: Session | null }> = ({ Component, @@ -10,7 +11,7 @@ const MyApp: AppType<{ session: Session | null }> = ({ }) => { return ( - + diff --git a/src/pages/experiments/[id].tsx b/src/pages/experiments/[id].tsx index ff3c024..ac9818a 100644 --- a/src/pages/experiments/[id].tsx +++ b/src/pages/experiments/[id].tsx @@ -24,7 +24,7 @@ export default function Experiment() { return ( - + diff --git a/src/server/api/routers/scenarios.router.ts b/src/server/api/routers/scenarios.router.ts index 2e5130a..96c4684 100644 --- a/src/server/api/routers/scenarios.router.ts +++ b/src/server/api/routers/scenarios.router.ts @@ -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 .input( z.object({ diff --git a/src/utils/theme.ts b/src/utils/theme.ts new file mode 100644 index 0000000..cecce29 --- /dev/null +++ b/src/utils/theme.ts @@ -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;