hiding and reordering scenarios
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { type DragEvent } from "react";
|
||||
import { api } from "~/utils/api";
|
||||
import { isEqual } from "lodash";
|
||||
import { type Scenario } from "./types";
|
||||
@@ -5,12 +6,21 @@ import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
||||
import { useState } from "react";
|
||||
import ResizeTextarea from "react-textarea-autosize";
|
||||
|
||||
import { Box, Button, Flex, HStack, Stack, Textarea } from "@chakra-ui/react";
|
||||
import { Box, Button, Flex, HStack, Icon, Stack, Textarea, Tooltip } from "@chakra-ui/react";
|
||||
import { cellPadding } from "../constants";
|
||||
import { BsX } from "react-icons/bs";
|
||||
import { RiDraggable } from "react-icons/ri";
|
||||
|
||||
export default function ScenarioEditor({ scenario }: { scenario: Scenario }) {
|
||||
export default function ScenarioEditor({
|
||||
scenario,
|
||||
hovered,
|
||||
}: {
|
||||
scenario: Scenario;
|
||||
hovered: boolean;
|
||||
}) {
|
||||
const savedValues = scenario.variableValues as Record<string, string>;
|
||||
const utils = api.useContext();
|
||||
const [isDragTarget, setIsDragTarget] = useState(false);
|
||||
|
||||
const [values, setValues] = useState<Record<string, string>>(savedValues);
|
||||
|
||||
@@ -30,60 +40,133 @@ export default function ScenarioEditor({ scenario }: { scenario: Scenario }) {
|
||||
await utils.scenarios.list.invalidate();
|
||||
}, [mutation, values]);
|
||||
|
||||
const hideMutation = api.scenarios.hide.useMutation();
|
||||
const [onHide] = useHandledAsyncCallback(async () => {
|
||||
await hideMutation.mutateAsync({
|
||||
id: scenario.id,
|
||||
});
|
||||
await utils.scenarios.list.invalidate();
|
||||
}, [hideMutation, scenario.id]);
|
||||
|
||||
const reorderMutation = api.scenarios.reorder.useMutation();
|
||||
const [onReorder] = useHandledAsyncCallback(
|
||||
async (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragTarget(false);
|
||||
const draggedId = e.dataTransfer.getData("text/plain");
|
||||
const droppedId = scenario.id;
|
||||
if (!draggedId || !droppedId || draggedId === droppedId) return;
|
||||
await reorderMutation.mutateAsync({
|
||||
draggedId,
|
||||
droppedId,
|
||||
});
|
||||
await utils.scenarios.list.invalidate();
|
||||
},
|
||||
[reorderMutation, scenario.id]
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack px={cellPadding.x} py={cellPadding.y}>
|
||||
{variableLabels.map((key) => {
|
||||
const value = values[key] ?? "";
|
||||
const layoutDirection = value.length > 20 ? "column" : "row";
|
||||
return (
|
||||
<Flex
|
||||
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
|
||||
borderRadius={0}
|
||||
px={2}
|
||||
py={1}
|
||||
placeholder="empty"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValues((prev) => ({ ...prev, [key]: e.target.value }));
|
||||
}}
|
||||
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", bg: "white" }}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
{hasChanged && (
|
||||
<HStack justify="right">
|
||||
<HStack
|
||||
pr={cellPadding.x}
|
||||
py={cellPadding.y}
|
||||
height="100%"
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.setData("text/plain", scenario.id);
|
||||
e.currentTarget.style.opacity = "0.4";
|
||||
}}
|
||||
onDragEnd={(e) => {
|
||||
e.currentTarget.style.opacity = "1";
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
setIsDragTarget(true);
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
setIsDragTarget(false);
|
||||
}}
|
||||
onDrop={onReorder}
|
||||
backgroundColor={isDragTarget ? "gray.100" : "transparent"}
|
||||
>
|
||||
<Stack alignSelf="flex-start" opacity={hovered ? 1 : 0}>
|
||||
<Tooltip label="Hide scenario" hasArrow>
|
||||
{/* for some reason the tooltip can't position itself properly relative to the icon without the wrapping box */}
|
||||
<Button
|
||||
size="sm"
|
||||
borderRadius={0}
|
||||
onMouseDown={() => {
|
||||
setValues(savedValues);
|
||||
variant="unstyled"
|
||||
color="gray.400"
|
||||
height="unset"
|
||||
width="unset"
|
||||
minW="unset"
|
||||
onClick={onHide}
|
||||
_hover={{
|
||||
color: "gray.800",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
colorScheme="gray"
|
||||
>
|
||||
Reset
|
||||
<Icon as={BsX} boxSize={6} />
|
||||
</Button>
|
||||
<Button size="sm" borderRadius={0} onMouseDown={onSave} colorScheme="blue">
|
||||
Save
|
||||
</Button>
|
||||
</HStack>
|
||||
)}
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
<Icon
|
||||
as={RiDraggable}
|
||||
boxSize={6}
|
||||
color="gray.400"
|
||||
_hover={{ color: "gray.800", cursor: "pointer" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
{variableLabels.map((key) => {
|
||||
const value = values[key] ?? "";
|
||||
const layoutDirection = value.length > 20 ? "column" : "row";
|
||||
return (
|
||||
<Flex
|
||||
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
|
||||
borderRadius={0}
|
||||
px={2}
|
||||
py={1}
|
||||
placeholder="empty"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValues((prev) => ({ ...prev, [key]: e.target.value }));
|
||||
}}
|
||||
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", bg: "white" }}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
{hasChanged && (
|
||||
<HStack justify="right">
|
||||
<Button
|
||||
size="sm"
|
||||
borderRadius={0}
|
||||
onMouseDown={() => {
|
||||
setValues(savedValues);
|
||||
}}
|
||||
colorScheme="gray"
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button size="sm" borderRadius={0} onMouseDown={onSave} colorScheme="blue">
|
||||
Save
|
||||
</Button>
|
||||
</HStack>
|
||||
)}
|
||||
</Stack>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useRef, useState, type DragEvent } from "react";
|
||||
import { type PromptVariant } from "./types";
|
||||
import { api } from "~/utils/api";
|
||||
import { useHandledAsyncCallback } from "~/utils/hooks";
|
||||
@@ -32,9 +32,9 @@ export default function VariantHeader(props: { variant: PromptVariant }) {
|
||||
|
||||
const reorderMutation = api.promptVariants.reorder.useMutation();
|
||||
const [onReorder] = useHandledAsyncCallback(
|
||||
async (e: DragEvent) => {
|
||||
async (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
console.log("onDrop");
|
||||
setIsDragTarget(false);
|
||||
const draggedId = e.dataTransfer.getData("text/plain");
|
||||
const droppedId = props.variant.id;
|
||||
if (!draggedId || !droppedId || draggedId === droppedId) return;
|
||||
|
||||
@@ -31,7 +31,7 @@ const ScenarioRow = (props: { scenario: Scenario; variants: PromptVariant[] }) =
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
sx={isHovered ? highlightStyle : undefined}
|
||||
>
|
||||
<ScenarioEditor scenario={props.scenario} />
|
||||
<ScenarioEditor scenario={props.scenario} hovered={isHovered} />
|
||||
</GridItem>
|
||||
{props.variants.map((variant) => (
|
||||
<GridItem
|
||||
@@ -64,7 +64,7 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
||||
<Grid
|
||||
p={4}
|
||||
display="grid"
|
||||
gridTemplateColumns={`200px repeat(${variants.data.length}, minmax(300px, 1fr)) auto`}
|
||||
gridTemplateColumns={`250px repeat(${variants.data.length}, minmax(300px, 1fr)) auto`}
|
||||
sx={{
|
||||
"> *": {
|
||||
borderColor: "gray.300",
|
||||
@@ -94,7 +94,6 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
||||
// Have to use `style` instead of emotion style props to work around css specificity issues conflicting with the "> *" selector on Grid
|
||||
style={{ borderRightWidth: 0, borderBottomWidth: 0 }}
|
||||
sx={stickyHeaderStyle}
|
||||
className="new-variant-button"
|
||||
>
|
||||
<NewVariantButton />
|
||||
</GridItem>
|
||||
|
||||
@@ -83,12 +83,8 @@ export const promptVariantsRouter = createTRPCRouter({
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await prisma.promptVariant.update({
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
data: {
|
||||
visible: false,
|
||||
},
|
||||
where: { id: input.id },
|
||||
data: { visible: false },
|
||||
});
|
||||
}),
|
||||
|
||||
@@ -185,7 +181,7 @@ export const promptVariantsRouter = createTRPCRouter({
|
||||
|
||||
// Find the index of the dragged item and the dropped item
|
||||
const dragIndex = visibleItems.findIndex((item) => item.id === dragged.id);
|
||||
const dropIndex = orderedItems.findIndex((item) => item.id === dropped.id);
|
||||
const dropIndex = visibleItems.findIndex((item) => item.id === dropped.id);
|
||||
|
||||
// Determine the new index for the dragged item
|
||||
let newIndex;
|
||||
|
||||
@@ -43,6 +43,82 @@ export const scenariosRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
|
||||
hide: publicProcedure.input(z.object({ id: z.string() })).mutation(async ({ input }) => {
|
||||
return await prisma.testScenario.update({
|
||||
where: { id: input.id },
|
||||
data: { visible: false },
|
||||
});
|
||||
}),
|
||||
|
||||
reorder: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
draggedId: z.string(),
|
||||
droppedId: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const dragged = await prisma.testScenario.findUnique({
|
||||
where: {
|
||||
id: input.draggedId,
|
||||
},
|
||||
});
|
||||
|
||||
const dropped = await prisma.testScenario.findUnique({
|
||||
where: {
|
||||
id: input.droppedId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!dragged || !dropped || dragged.experimentId !== dropped.experimentId) {
|
||||
throw new Error(
|
||||
`Prompt Variant with id ${input.draggedId} or ${input.droppedId} does not exist`
|
||||
);
|
||||
}
|
||||
|
||||
const visibleItems = await prisma.testScenario.findMany({
|
||||
where: {
|
||||
experimentId: dragged.experimentId,
|
||||
visible: true,
|
||||
},
|
||||
orderBy: {
|
||||
sortIndex: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
// Remove the dragged item from its current position
|
||||
const orderedItems = visibleItems.filter((item) => item.id !== dragged.id);
|
||||
|
||||
// Find the index of the dragged item and the dropped item
|
||||
const dragIndex = visibleItems.findIndex((item) => item.id === dragged.id);
|
||||
const dropIndex = visibleItems.findIndex((item) => item.id === dropped.id);
|
||||
|
||||
// Determine the new index for the dragged item
|
||||
let newIndex;
|
||||
if (dragIndex < dropIndex) {
|
||||
newIndex = dropIndex + 1; // Insert after the dropped item
|
||||
} else {
|
||||
newIndex = dropIndex; // Insert before the dropped item
|
||||
}
|
||||
|
||||
// Insert the dragged item at the new position
|
||||
orderedItems.splice(newIndex, 0, dragged);
|
||||
|
||||
// Now, we need to update all the items with their new sortIndex
|
||||
await prisma.$transaction(
|
||||
orderedItems.map((item, index) => {
|
||||
return prisma.testScenario.update({
|
||||
where: {
|
||||
id: item.id,
|
||||
},
|
||||
data: {
|
||||
sortIndex: index,
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
}),
|
||||
|
||||
replaceWithValues: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
|
||||
Reference in New Issue
Block a user