Make it so you can't delete the last prompt or scenario

No reason for an experiment to have 0 prompts or 0 scenarios and it makes the UI look bad.
This commit is contained in:
Kyle Corbitt
2023-07-14 15:49:42 -07:00
parent b98eb9b729
commit 26ee8698be
8 changed files with 77 additions and 40 deletions

View File

@@ -13,10 +13,11 @@ import AutoResizeTextArea from "../AutoResizeTextArea";
export default function ScenarioEditor({
scenario,
hovered,
...props
}: {
scenario: Scenario;
hovered: boolean;
canHide: boolean;
}) {
const savedValues = scenario.variableValues as Record<string, string>;
const utils = api.useContext();
@@ -92,30 +93,34 @@ export default function ScenarioEditor({
onDrop={onReorder}
backgroundColor={isDragTarget ? "gray.100" : "transparent"}
>
<Stack alignSelf="flex-start" opacity={hovered ? 1 : 0} spacing={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
variant="unstyled"
color="gray.400"
height="unset"
width="unset"
minW="unset"
onClick={onHide}
_hover={{
color: "gray.800",
cursor: "pointer",
}}
>
<Icon as={hidingInProgress ? Spinner : BsX} boxSize={6} />
</Button>
</Tooltip>
<Icon
as={RiDraggable}
boxSize={6}
color="gray.400"
_hover={{ color: "gray.800", cursor: "pointer" }}
/>
<Stack alignSelf="flex-start" opacity={props.hovered ? 1 : 0} spacing={0}>
{props.canHide && (
<>
<Tooltip label="Hide scenario" hasArrow>
{/* for some reason the tooltip can't position itself properly relative to the icon without the wrapping box */}
<Button
variant="unstyled"
color="gray.400"
height="unset"
width="unset"
minW="unset"
onClick={onHide}
_hover={{
color: "gray.800",
cursor: "pointer",
}}
>
<Icon as={hidingInProgress ? Spinner : BsX} boxSize={6} />
</Button>
</Tooltip>
<Icon
as={RiDraggable}
boxSize={6}
color="gray.400"
_hover={{ color: "gray.800", cursor: "pointer" }}
/>
</>
)}
</Stack>
{variableLabels.length === 0 ? (
<Box color="gray.500">{vars.data ? "No scenario variables configured" : "Loading..."}</Box>

View File

@@ -5,7 +5,11 @@ import OutputCell from "./OutputCell/OutputCell";
import ScenarioEditor from "./ScenarioEditor";
import type { PromptVariant, Scenario } from "./types";
const ScenarioRow = (props: { scenario: Scenario; variants: PromptVariant[] }) => {
const ScenarioRow = (props: {
scenario: Scenario;
variants: PromptVariant[];
canHide: boolean;
}) => {
const [isHovered, setIsHovered] = useState(false);
const highlightStyle = { backgroundColor: "gray.50" };
@@ -18,7 +22,7 @@ const ScenarioRow = (props: { scenario: Scenario; variants: PromptVariant[] }) =
sx={isHovered ? highlightStyle : undefined}
borderLeftWidth={1}
>
<ScenarioEditor scenario={props.scenario} hovered={isHovered} />
<ScenarioEditor scenario={props.scenario} hovered={isHovered} canHide={props.canHide} />
</GridItem>
{props.variants.map((variant) => (
<GridItem

View File

@@ -8,7 +8,7 @@ import { RiDraggable } from "react-icons/ri";
import { cellPadding, headerMinHeight } from "../constants";
import AutoResizeTextArea from "../AutoResizeTextArea";
export default function VariantHeader(props: { variant: PromptVariant }) {
export default function VariantHeader(props: { variant: PromptVariant; canHide: boolean }) {
const utils = api.useContext();
const [isDragTarget, setIsDragTarget] = useState(false);
const [isInputHovered, setIsInputHovered] = useState(false);
@@ -95,11 +95,13 @@ export default function VariantHeader(props: { variant: PromptVariant }) {
onMouseEnter={() => setIsInputHovered(true)}
onMouseLeave={() => setIsInputHovered(false)}
/>
<Tooltip label="Hide Variant" hasArrow>
<Button variant="ghost" colorScheme="gray" size="sm" onClick={onHide}>
<Icon as={BsX} boxSize={6} />
</Button>
</Tooltip>
{props.canHide && (
<Tooltip label="Remove Variant" hasArrow>
<Button variant="ghost" colorScheme="gray" size="sm" onClick={onHide}>
<Icon as={BsX} boxSize={6} />
</Button>
</Tooltip>
)}
</HStack>
);
}

View File

@@ -78,7 +78,7 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
{variants.data.map((variant) => (
<GridItem key={variant.uiId} padding={0} sx={stickyHeaderStyle} borderTopWidth={1}>
<VariantHeader variant={variant} />
<VariantHeader variant={variant} canHide={variants.data.length > 1} />
</GridItem>
))}
<GridItem
@@ -103,7 +103,12 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
</GridItem>
))}
{scenarios.data.map((scenario) => (
<ScenarioRow key={scenario.uiId} scenario={scenario} variants={variants.data} />
<ScenarioRow
key={scenario.uiId}
scenario={scenario}
variants={variants.data}
canHide={scenarios.data.length > 1}
/>
))}
<GridItem borderBottomWidth={0} borderRightWidth={0} w="100%" colSpan={allCols} padding={0}>
<NewScenarioButton />

View File

@@ -74,15 +74,23 @@ export const experimentsRouter = createTRPCRouter({
constructFn: dedent`prompt = {
model: "gpt-3.5-turbo-0613",
stream: true,
messages: [{ role: "system", content: "Return 'Ready to go!'" }],
messages: [{ role: "system", content: ${"`Return '${scenario.text}'`"} }],
}`,
model: "gpt-3.5-turbo-0613",
},
}),
prisma.templateVariable.create({
data: {
experimentId: exp.id,
label: "text",
},
}),
prisma.testScenario.create({
data: {
experimentId: exp.id,
variableValues: {},
variableValues: {
text: "This is a test scenario.",
},
},
}),
]);

View File

@@ -1,3 +1,4 @@
import dedent from "dedent";
import { isObject } from "lodash";
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
@@ -113,7 +114,18 @@ export const promptVariantsRouter = createTRPCRouter({
experimentId: input.experimentId,
label: `Prompt Variant ${largestSortIndex + 2}`,
sortIndex: (lastVariant?.sortIndex ?? 0) + 1,
constructFn: lastVariant?.constructFn ?? "",
constructFn:
lastVariant?.constructFn ??
dedent`
prompt = {
model: "gpt-3.5-turbo",
messages: [
{
role: "system",
content: "Return 'Hello, world!'",
}
]
}`,
model: lastVariant?.model ?? "gpt-3.5-turbo",
},
});

View File

@@ -9,7 +9,7 @@ export async function constructPrompt(
scenario: TestScenario["variableValues"],
): Promise<JSONSerializable> {
const code = `
const scenario = ${JSON.stringify(scenario, null, 2)};
const scenario = ${JSON.stringify(scenario ?? {}, null, 2)};
let prompt
${variant.constructFn}

View File

@@ -33,6 +33,7 @@ export const createVariantEditorSlice: SliceCreator<SharedVariantEditorSlice> =
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
allowNonTsExtensions: true,
strictNullChecks: true,
lib: ["esnext"],
});
@@ -84,7 +85,7 @@ export const createVariantEditorSlice: SliceCreator<SharedVariantEditorSlice> =
)} as const;
type Scenario = typeof scenarios[number];
declare var scenario: Scenario | null;
declare var scenario: Scenario | { [key: string]: string };
`;
const scenariosModel = monaco.editor.getModel(monaco.Uri.parse("file:///scenarios.ts"));