prettifying the ux
This commit is contained in:
@@ -1,40 +0,0 @@
|
||||
import { useRef } from "react";
|
||||
import { type PromptVariant } from "./types";
|
||||
import { api } from "~/utils/api";
|
||||
import { useHandledAsyncCallback } from "~/utils/hooks";
|
||||
import { Heading } from "@chakra-ui/react";
|
||||
|
||||
export default function EditableVariantLabel(props: { variant: PromptVariant }) {
|
||||
const labelRef = useRef<HTMLHeadingElement | null>(null);
|
||||
|
||||
const mutation = api.promptVariants.update.useMutation();
|
||||
|
||||
const [onBlur] = useHandledAsyncCallback(async () => {
|
||||
const newLabel = labelRef.current?.innerText;
|
||||
if (newLabel && newLabel !== props.variant.label) {
|
||||
await mutation.mutateAsync({
|
||||
id: props.variant.id,
|
||||
updates: { label: newLabel },
|
||||
});
|
||||
}
|
||||
}, [mutation, props.variant.id, props.variant.label]);
|
||||
|
||||
return (
|
||||
<Heading
|
||||
fontWeight="bold"
|
||||
size="md"
|
||||
ref={labelRef}
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
borderWidth={1}
|
||||
borderColor="transparent"
|
||||
_hover={{ borderColor: "gray.300" }}
|
||||
_focus={{ borderColor: "blue.500", outline: "none" }}
|
||||
onBlur={onBlur}
|
||||
px={4}
|
||||
py={2}
|
||||
>
|
||||
{props.variant.label}
|
||||
</Heading>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Button, Tooltip } from "@chakra-ui/react";
|
||||
import { BsPlus } from "react-icons/bs";
|
||||
import { api } from "~/utils/api";
|
||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
||||
import { cellPadding, headerMinHeight } from "../constants";
|
||||
|
||||
export default function NewVariantButton() {
|
||||
const experiment = useExperiment();
|
||||
@@ -21,16 +22,19 @@ export default function NewVariantButton() {
|
||||
<Button
|
||||
w="100%"
|
||||
borderRadius={0}
|
||||
alignItems="flex-start"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
fontWeight="normal"
|
||||
bgColor="blue.100"
|
||||
_hover={{ bgColor: "blue.200" }}
|
||||
py={2}
|
||||
px={0}
|
||||
bgColor="transparent"
|
||||
_hover={{ bgColor: "gray.100" }}
|
||||
px={cellPadding.x}
|
||||
// py={cellPadding.y}
|
||||
onClick={onClick}
|
||||
height="unset"
|
||||
minH={headerMinHeight}
|
||||
>
|
||||
<BsPlus size={24} />
|
||||
New Variant
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,13 @@ import { PromptVariant, Scenario } from "./types";
|
||||
import { Center, Spinner, Text } from "@chakra-ui/react";
|
||||
import { useExperiment } from "~/utils/hooks";
|
||||
import { JSONSerializable } from "~/server/types";
|
||||
import { cellPadding } from "../constants";
|
||||
|
||||
const CellShell = ({ children }: { children: React.ReactNode }) => (
|
||||
<Center h="100%" w="100%" px={cellPadding.x} py={cellPadding.y}>
|
||||
{children}
|
||||
</Center>
|
||||
);
|
||||
|
||||
export default function OutputCell({
|
||||
scenario,
|
||||
@@ -36,23 +43,21 @@ export default function OutputCell({
|
||||
|
||||
if (disabledReason)
|
||||
return (
|
||||
<Center h="100%">
|
||||
<CellShell>
|
||||
<Text color="gray.500">{disabledReason}</Text>
|
||||
</Center>
|
||||
</CellShell>
|
||||
);
|
||||
|
||||
if (output.isLoading)
|
||||
return (
|
||||
<Center h="100%">
|
||||
<CellShell>
|
||||
<Spinner />
|
||||
</Center>
|
||||
</CellShell>
|
||||
);
|
||||
|
||||
if (!output.data) return <Center h="100%">No output</Center>;
|
||||
if (!output.data) return <CellShell>No output</CellShell>;
|
||||
|
||||
return (
|
||||
<Center h="100%">
|
||||
{JSON.stringify(output.data.output.choices[0].message.content, null, 2)}
|
||||
</Center>
|
||||
<CellShell>{JSON.stringify(output.data.output.choices[0].message.content, null, 2)}</CellShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ import { useState } from "react";
|
||||
import ResizeTextarea from "react-textarea-autosize";
|
||||
|
||||
import { Box, Button, Flex, HStack, Stack, Textarea } from "@chakra-ui/react";
|
||||
import { cellPadding } from "../constants";
|
||||
|
||||
export default function ScenarioHeader({ scenario }: { scenario: Scenario }) {
|
||||
export default function ScenarioEditor({ scenario }: { scenario: Scenario }) {
|
||||
const savedValues = scenario.variableValues as Record<string, string>;
|
||||
const utils = api.useContext();
|
||||
|
||||
@@ -30,7 +31,7 @@ export default function ScenarioHeader({ scenario }: { scenario: Scenario }) {
|
||||
}, [mutation, values]);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack px={cellPadding.x} py={cellPadding.y}>
|
||||
{variableLabels.map((key) => {
|
||||
const value = values[key] ?? "";
|
||||
const layoutDirection = value.length > 20 ? "column" : "row";
|
||||
@@ -1,53 +1,66 @@
|
||||
import { useCallback } from "react";
|
||||
import type { PromptVariant } from "./types";
|
||||
import { useRef } from "react";
|
||||
import { type PromptVariant } from "./types";
|
||||
import { api } from "~/utils/api";
|
||||
import { type JSONSerializable } from "~/server/types";
|
||||
import VariantConfigEditor from "./VariantConfigEditor";
|
||||
import EditableVariantLabel from "./EditableVariantLabel";
|
||||
import { Stack, useToast } from "@chakra-ui/react";
|
||||
import { useHandledAsyncCallback } from "~/utils/hooks";
|
||||
import { Button, HStack, Heading, Tooltip } from "@chakra-ui/react";
|
||||
import { BsX } from "react-icons/bs";
|
||||
import { cellPadding, headerMinHeight } from "../constants";
|
||||
|
||||
export default function VariantHeader({ variant }: { variant: PromptVariant }) {
|
||||
const replaceWithConfig = api.promptVariants.replaceWithConfig.useMutation();
|
||||
export default function VariantHeader(props: { variant: PromptVariant }) {
|
||||
const utils = api.useContext();
|
||||
const toast = useToast();
|
||||
|
||||
const onSave = useCallback(
|
||||
async (currentConfig: string) => {
|
||||
let parsedConfig: JSONSerializable;
|
||||
try {
|
||||
parsedConfig = JSON.parse(currentConfig) as JSONSerializable;
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: "Invalid JSON",
|
||||
description: "Please fix the JSON before saving.",
|
||||
status: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedConfig === null) {
|
||||
toast({
|
||||
title: "Invalid JSON",
|
||||
description: "Please fix the JSON before saving.",
|
||||
status: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await replaceWithConfig.mutateAsync({
|
||||
id: variant.id,
|
||||
config: currentConfig,
|
||||
const labelRef = useRef<HTMLHeadingElement | null>(null);
|
||||
const updateMutation = api.promptVariants.update.useMutation();
|
||||
const [onSaveLabel] = useHandledAsyncCallback(async () => {
|
||||
const newLabel = labelRef.current?.innerText;
|
||||
if (newLabel && newLabel !== props.variant.label) {
|
||||
await updateMutation.mutateAsync({
|
||||
id: props.variant.id,
|
||||
updates: { label: newLabel },
|
||||
});
|
||||
}
|
||||
}, [updateMutation, props.variant.id, props.variant.label]);
|
||||
|
||||
await utils.promptVariants.list.invalidate();
|
||||
},
|
||||
[variant.id, replaceWithConfig, utils.promptVariants.list, toast]
|
||||
);
|
||||
const hideMutation = api.promptVariants.hide.useMutation();
|
||||
const [onHide] = useHandledAsyncCallback(async () => {
|
||||
await hideMutation.mutateAsync({
|
||||
id: props.variant.id,
|
||||
});
|
||||
await utils.promptVariants.list.invalidate();
|
||||
}, [hideMutation, props.variant.id]);
|
||||
|
||||
return (
|
||||
<Stack w="100%">
|
||||
<EditableVariantLabel variant={variant} />
|
||||
<VariantConfigEditor variant={variant} />
|
||||
</Stack>
|
||||
<HStack spacing={4} alignItems="center" minH={headerMinHeight}>
|
||||
<Heading
|
||||
fontWeight="bold"
|
||||
size="md"
|
||||
ref={labelRef}
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
borderWidth={1}
|
||||
borderColor="transparent"
|
||||
_hover={{ borderColor: "gray.300" }}
|
||||
_focus={{ borderColor: "blue.500", outline: "none" }}
|
||||
onBlur={onSaveLabel}
|
||||
flex={1}
|
||||
px={cellPadding.x}
|
||||
// py={cellPadding.y}
|
||||
>
|
||||
{props.variant.label}
|
||||
</Heading>
|
||||
<Tooltip label="Hide Variant" hasArrow>
|
||||
<Button
|
||||
variant="ghost"
|
||||
colorScheme="gray"
|
||||
size="sm"
|
||||
onClick={onHide}
|
||||
borderRadius={0}
|
||||
// px={cellPadding.x}
|
||||
// py={cellPadding.y}
|
||||
>
|
||||
<BsX size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { RouterOutputs, api } from "~/utils/api";
|
||||
import { Scenario, type PromptVariant } from "./types";
|
||||
import VariantHeader from "./VariantHeader";
|
||||
import OutputCell from "./OutputCell";
|
||||
import ScenarioHeader from "./ScenarioHeader";
|
||||
import ScenarioEditor from "./ScenarioEditor";
|
||||
import React, { useState } from "react";
|
||||
import { Box, Grid, GridItem, Heading } from "@chakra-ui/react";
|
||||
import { Box, Grid, GridItem, Heading, SystemStyleObject } from "@chakra-ui/react";
|
||||
import NewScenarioButton from "./NewScenarioButton";
|
||||
import NewVariantButton from "./NewVariantButton";
|
||||
import EditableVariantLabel from "./EditableVariantLabel";
|
||||
import VariantHeader from "./VariantHeader";
|
||||
import VariantConfigEditor from "./VariantConfigEditor";
|
||||
|
||||
const stickyHeaderStyle: SystemStyleObject = {
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
backgroundColor: "#fff",
|
||||
zIndex: 1,
|
||||
};
|
||||
|
||||
const ScenarioRow = (props: { scenario: Scenario; variants: PromptVariant[] }) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
@@ -22,16 +28,16 @@ const ScenarioRow = (props: { scenario: Scenario; variants: PromptVariant[] }) =
|
||||
<GridItem
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
sx={isHovered ? highlightStyle : null}
|
||||
sx={isHovered ? highlightStyle : undefined}
|
||||
>
|
||||
<ScenarioHeader scenario={props.scenario} />
|
||||
<ScenarioEditor scenario={props.scenario} />
|
||||
</GridItem>
|
||||
{props.variants.map((variant) => (
|
||||
<GridItem
|
||||
key={variant.id}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
sx={isHovered ? highlightStyle : null}
|
||||
sx={isHovered ? highlightStyle : undefined}
|
||||
>
|
||||
<OutputCell key={variant.id} scenario={props.scenario} variant={variant} />
|
||||
</GridItem>
|
||||
@@ -57,14 +63,12 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
||||
<Grid
|
||||
p={4}
|
||||
display="grid"
|
||||
gridTemplateColumns={`200px repeat(${variants.data.length}, minmax(300px, 1fr)) 40px`}
|
||||
gridTemplateColumns={`200px repeat(${variants.data.length}, minmax(300px, 1fr)) auto`}
|
||||
sx={{
|
||||
"> *": {
|
||||
borderColor: "gray.300",
|
||||
borderBottomWidth: 1,
|
||||
borderRightWidth: 1,
|
||||
paddingX: 4,
|
||||
paddingY: 2,
|
||||
},
|
||||
"> *:last-child": {
|
||||
borderRightWidth: 0,
|
||||
@@ -72,25 +76,24 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
||||
}}
|
||||
>
|
||||
<GridItem display="flex" alignItems="flex-end" rowSpan={2}>
|
||||
<Heading size="md" fontWeight="bold">
|
||||
Scenario
|
||||
</Heading>
|
||||
<Box sx={stickyHeaderStyle} flex={1}>
|
||||
<Heading size="md" fontWeight="bold">
|
||||
Scenario
|
||||
</Heading>
|
||||
</Box>
|
||||
</GridItem>
|
||||
{variants.data.map((variant) => (
|
||||
<GridItem
|
||||
key={variant.uiId}
|
||||
padding={0}
|
||||
sx={{ position: "sticky", top: 0, backgroundColor: "#fff", zIndex: 1 }}
|
||||
>
|
||||
<EditableVariantLabel variant={variant} />
|
||||
<GridItem key={variant.uiId} padding={0} sx={stickyHeaderStyle}>
|
||||
<VariantHeader variant={variant} />
|
||||
</GridItem>
|
||||
))}
|
||||
<GridItem
|
||||
borderBottomWidth={0}
|
||||
rowSpan={scenarios.data.length + 1}
|
||||
padding={0}
|
||||
borderRightWidth={0}
|
||||
sx={{ position: "sticky", top: 0, backgroundColor: "#fff", zIndex: 1 }}
|
||||
// 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>
|
||||
|
||||
Reference in New Issue
Block a user