diff --git a/src/components/OutputsTable/AddVariantButton.tsx b/src/components/OutputsTable/AddVariantButton.tsx new file mode 100644 index 0000000..460b47e --- /dev/null +++ b/src/components/OutputsTable/AddVariantButton.tsx @@ -0,0 +1,48 @@ +import { Box, Flex, Icon, Spinner } from "@chakra-ui/react"; +import { BsPlus } from "react-icons/bs"; +import { api } from "~/utils/api"; +import { useExperiment, useExperimentAccess, useHandledAsyncCallback } 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 [onClick, loading] = useHandledAsyncCallback(async () => { + if (!experiment.data) return; + await mutation.mutateAsync({ + experimentId: experiment.data.id, + }); + await utils.promptVariants.list.invalidate(); + }, [mutation]); + + const { canModify } = useExperimentAccess(); + if (!canModify) return ; + + return ( + + } + > + Add Variant + + {/* */} + + ); +} diff --git a/src/components/OutputsTable/NewScenarioButton.tsx b/src/components/OutputsTable/NewScenarioButton.tsx deleted file mode 100644 index 5542338..0000000 --- a/src/components/OutputsTable/NewScenarioButton.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Button, type ButtonProps, HStack, Spinner, Icon } from "@chakra-ui/react"; -import { BsPlus } from "react-icons/bs"; -import { api } from "~/utils/api"; -import { useExperiment, useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks"; - -// Extracted Button styling into reusable component -const StyledButton = ({ children, onClick }: ButtonProps) => ( - -); - -export default function NewScenarioButton() { - const { canModify } = useExperimentAccess(); - - const experiment = useExperiment(); - const mutation = api.scenarios.create.useMutation(); - const utils = api.useContext(); - - const [onClick] = useHandledAsyncCallback(async () => { - if (!experiment.data) return; - await mutation.mutateAsync({ - experimentId: experiment.data.id, - }); - await utils.scenarios.list.invalidate(); - }, [mutation]); - - const [onAutogenerate, autogenerating] = useHandledAsyncCallback(async () => { - if (!experiment.data) return; - await mutation.mutateAsync({ - experimentId: experiment.data.id, - autogenerate: true, - }); - await utils.scenarios.list.invalidate(); - }, [mutation]); - - if (!canModify) return null; - - return ( - - - - Add Scenario - - - - Autogenerate Scenario - - - ); -} diff --git a/src/components/OutputsTable/NewVariantButton.tsx b/src/components/OutputsTable/NewVariantButton.tsx deleted file mode 100644 index f2ddfdd..0000000 --- a/src/components/OutputsTable/NewVariantButton.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Box, Button, Icon, Spinner, Text } from "@chakra-ui/react"; -import { BsPlus } from "react-icons/bs"; -import { api } from "~/utils/api"; -import { useExperiment, useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks"; -import { cellPadding, headerMinHeight } from "../constants"; - -export default function NewVariantButton() { - const experiment = useExperiment(); - const mutation = api.promptVariants.create.useMutation(); - const utils = api.useContext(); - - const [onClick, loading] = useHandledAsyncCallback(async () => { - if (!experiment.data) return; - await mutation.mutateAsync({ - experimentId: experiment.data.id, - }); - await utils.promptVariants.list.invalidate(); - }, [mutation]); - - const { canModify } = useExperimentAccess(); - if (!canModify) return ; - - return ( - - ); -} diff --git a/src/components/OutputsTable/ScenarioRow.tsx b/src/components/OutputsTable/ScenarioRow.tsx index 4431318..d9544ce 100644 --- a/src/components/OutputsTable/ScenarioRow.tsx +++ b/src/components/OutputsTable/ScenarioRow.tsx @@ -4,11 +4,13 @@ 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,15 +23,21 @@ const ScenarioRow = (props: { onMouseLeave={() => setIsHovered(false)} sx={isHovered ? highlightStyle : undefined} borderLeftWidth={1} + {...borders} + rowStart={props.rowStart} + colStart={1} > - {props.variants.map((variant) => ( + {props.variants.map((variant, i) => ( setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} sx={isHovered ? highlightStyle : undefined} + rowStart={props.rowStart} + colStart={i + 2} + {...borders} > diff --git a/src/components/OutputsTable/ScenariosHeader.tsx b/src/components/OutputsTable/ScenariosHeader.tsx index c9a79e7..f3ccdc6 100644 --- a/src/components/OutputsTable/ScenariosHeader.tsx +++ b/src/components/OutputsTable/ScenariosHeader.tsx @@ -1,52 +1,73 @@ -import { Button, GridItem, HStack, Heading } from "@chakra-ui/react"; +import { + Button, + type ButtonProps, + HStack, + Text, + Icon, + Menu, + MenuButton, + MenuList, + MenuItem, + IconButton, + Spinner, +} from "@chakra-ui/react"; import { cellPadding } from "../constants"; -import { useElementDimensions, useExperimentAccess } from "~/utils/hooks"; -import { stickyHeaderStyle } from "./styles"; -import { BsPencil } from "react-icons/bs"; +import { useExperiment, useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks"; +import { BsGear, BsPencil, BsPlus, BsStars } from "react-icons/bs"; import { useAppStore } from "~/state/store"; +import { api } from "~/utils/api"; -export const ScenariosHeader = ({ - headerRows, - numScenarios, -}: { - headerRows: number; - numScenarios: number; -}) => { +export const ActionButton = (props: ButtonProps) => ( + - )} - - + + + Scenarios ({props.numScenarios}) + + {canModify && ( + + + } + /> + + + } onClick={() => onAddScenario(false)}> + Add Scenario + + } onClick={() => onAddScenario(true)}> + Autogenerate Scenario + + } onClick={openDrawer}> + Edit Vars + + + + )} + ); }; diff --git a/src/components/OutputsTable/index.tsx b/src/components/OutputsTable/index.tsx index 00bd471..4b6fbb9 100644 --- a/src/components/OutputsTable/index.tsx +++ b/src/components/OutputsTable/index.tsx @@ -1,13 +1,12 @@ -import { Grid, GridItem } from "@chakra-ui/react"; +import { Grid, GridItem, type GridItemProps } from "@chakra-ui/react"; import { api } from "~/utils/api"; -import NewScenarioButton from "./NewScenarioButton"; -import NewVariantButton from "./NewVariantButton"; +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 { stickyHeaderStyle } from "./styles"; +import { borders } from "./styles"; export default function OutputsTable({ experimentId }: { experimentId: string | undefined }) { const variants = api.promptVariants.list.useQuery( @@ -22,61 +21,76 @@ export default function OutputsTable({ experimentId }: { experimentId: string | if (!variants.data || !scenarios.data) return null; - const allCols = variants.data.length + 1; - const headerRows = 3; + const allCols = variants.data.length + 2; + const variantHeaderRows = 3; + const scenarioHeaderRows = 1; + const allRows = variantHeaderRows + scenarioHeaderRows + scenarios.data.length; return ( *": { borderColor: "gray.300", - borderBottomWidth: 1, - borderRightWidth: 1, }, }} fontSize="sm" > - - - {variants.data.map((variant) => ( - 1} /> - ))} - *" selector on Grid - style={{ borderRightWidth: 0, borderBottomWidth: 0 }} - h={8} - sx={stickyHeaderStyle} - > - + + - {variants.data.map((variant) => ( - - - - ))} - {variants.data.map((variant) => ( - - - - ))} - {scenarios.data.map((scenario) => ( + {variants.data.map((variant, i) => { + const sharedProps: GridItemProps = { + ...borders, + colStart: i + 2, + borderLeftWidth: i === 0 ? 1 : 0, + }; + return ( + <> + 1} + rowStart={1} + {...sharedProps} + /> + + + + + + + + ); + })} + + + + + + {scenarios.data.map((scenario, i) => ( 1} /> ))} - - - + + {/* 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. */} + ); } diff --git a/src/components/OutputsTable/styles.ts b/src/components/OutputsTable/styles.ts index fbb6e67..234a22d 100644 --- a/src/components/OutputsTable/styles.ts +++ b/src/components/OutputsTable/styles.ts @@ -1,4 +1,4 @@ -import { type SystemStyleObject } from "@chakra-ui/react"; +import { type GridItemProps, type SystemStyleObject } from "@chakra-ui/react"; export const stickyHeaderStyle: SystemStyleObject = { position: "sticky", @@ -6,3 +6,8 @@ export const stickyHeaderStyle: SystemStyleObject = { backgroundColor: "#fff", zIndex: 10, }; + +export const borders: GridItemProps = { + borderRightWidth: 1, + borderBottomWidth: 1, +}; diff --git a/src/components/VariantHeader/VariantHeader.tsx b/src/components/VariantHeader/VariantHeader.tsx index 3c83316..a0e8482 100644 --- a/src/components/VariantHeader/VariantHeader.tsx +++ b/src/components/VariantHeader/VariantHeader.tsx @@ -3,28 +3,34 @@ import { type PromptVariant } from "../OutputsTable/types"; import { api } from "~/utils/api"; import { RiDraggable } from "react-icons/ri"; import { useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks"; -import { HStack, Icon, Text, GridItem } from "@chakra-ui/react"; // Changed here +import { HStack, Icon, Text, GridItem, type GridItemProps } from "@chakra-ui/react"; // Changed here import { cellPadding, headerMinHeight } from "../constants"; import AutoResizeTextArea from "../AutoResizeTextArea"; import { stickyHeaderStyle } from "../OutputsTable/styles"; import VariantHeaderMenuButton from "./VariantHeaderMenuButton"; -export default function VariantHeader(props: { variant: PromptVariant; canHide: boolean }) { +export default function VariantHeader( + allProps: { + variant: PromptVariant; + canHide: boolean; + } & GridItemProps, +) { + const { variant, canHide, ...gridItemProps } = allProps; const { canModify } = useExperimentAccess(); const utils = api.useContext(); const [isDragTarget, setIsDragTarget] = useState(false); const [isInputHovered, setIsInputHovered] = useState(false); - const [label, setLabel] = useState(props.variant.label); + const [label, setLabel] = useState(variant.label); const updateMutation = api.promptVariants.update.useMutation(); const [onSaveLabel] = useHandledAsyncCallback(async () => { - if (label && label !== props.variant.label) { + if (label && label !== variant.label) { await updateMutation.mutateAsync({ - id: props.variant.id, + id: variant.id, updates: { label: label }, }); } - }, [updateMutation, props.variant.id, props.variant.label, label]); + }, [updateMutation, variant.id, variant.label, label]); const reorderMutation = api.promptVariants.reorder.useMutation(); const [onReorder] = useHandledAsyncCallback( @@ -32,7 +38,7 @@ export default function VariantHeader(props: { variant: PromptVariant; canHide: e.preventDefault(); setIsDragTarget(false); const draggedId = e.dataTransfer.getData("text/plain"); - const droppedId = props.variant.id; + const droppedId = variant.id; if (!draggedId || !droppedId || draggedId === droppedId) return; await reorderMutation.mutateAsync({ draggedId, @@ -40,16 +46,16 @@ export default function VariantHeader(props: { variant: PromptVariant; canHide: }); await utils.promptVariants.list.invalidate(); }, - [reorderMutation, props.variant.id], + [reorderMutation, variant.id], ); const [menuOpen, setMenuOpen] = useState(false); if (!canModify) { return ( - + - {props.variant.label} + {variant.label} ); @@ -64,6 +70,7 @@ export default function VariantHeader(props: { variant: PromptVariant; canHide: zIndex: menuOpen ? "dropdown" : stickyHeaderStyle.zIndex, }} borderTopWidth={1} + {...gridItemProps} > { - e.dataTransfer.setData("text/plain", props.variant.id); + e.dataTransfer.setData("text/plain", variant.id); e.currentTarget.style.opacity = "0.4"; }} onDragEnd={(e) => { @@ -112,8 +119,8 @@ export default function VariantHeader(props: { variant: PromptVariant; canHide: onMouseLeave={() => setIsInputHovered(false)} /> diff --git a/src/server/api/routers/scenarios.router.ts b/src/server/api/routers/scenarios.router.ts index 91f1852..c8cdd4d 100644 --- a/src/server/api/routers/scenarios.router.ts +++ b/src/server/api/routers/scenarios.router.ts @@ -34,22 +34,21 @@ export const scenariosRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { await requireCanModifyExperiment(input.experimentId, ctx); - const maxSortIndex = - ( - await prisma.testScenario.aggregate({ - where: { - experimentId: input.experimentId, - }, - _max: { - sortIndex: true, - }, - }) - )._max.sortIndex ?? 0; + await prisma.testScenario.updateMany({ + where: { + experimentId: input.experimentId, + }, + data: { + sortIndex: { + increment: 1, + }, + }, + }); const createNewScenarioAction = prisma.testScenario.create({ data: { experimentId: input.experimentId, - sortIndex: maxSortIndex + 1, + sortIndex: 0, variableValues: input.autogenerate ? await autogenerateScenarioValues(input.experimentId) : {},