prettifying the ux

This commit is contained in:
Kyle Corbitt
2023-06-26 06:26:19 -07:00
parent cde22ac4bf
commit e6fdd2d5c5
13 changed files with 141 additions and 222 deletions

View File

@@ -16,8 +16,6 @@
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/server": "^11.11.0", "@emotion/server": "^11.11.0",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@fontsource/poppins": "^5.0.3",
"@fontsource/roboto": "^5.0.3",
"@monaco-editor/react": "^4.5.1", "@monaco-editor/react": "^4.5.1",
"@next-auth/prisma-adapter": "^1.0.5", "@next-auth/prisma-adapter": "^1.0.5",
"@prisma/client": "^4.14.0", "@prisma/client": "^4.14.0",

14
pnpm-lock.yaml generated
View File

@@ -20,12 +20,6 @@ dependencies:
'@emotion/styled': '@emotion/styled':
specifier: ^11.11.0 specifier: ^11.11.0
version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.6)(react@18.2.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': '@monaco-editor/react':
specifier: ^4.5.1 specifier: ^4.5.1
version: 4.5.1(monaco-editor@0.39.0)(react-dom@18.2.0)(react@18.2.0) version: 4.5.1(monaco-editor@0.39.0)(react-dom@18.2.0)(react@18.2.0)
@@ -1682,14 +1676,6 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true 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: /@humanwhocodes/config-array@0.11.10:
resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
engines: {node: '>=10.10.0'} engines: {node: '>=10.10.0'}

View File

@@ -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>
);
}

View File

@@ -2,6 +2,7 @@ import { Button, Tooltip } from "@chakra-ui/react";
import { BsPlus } from "react-icons/bs"; import { BsPlus } from "react-icons/bs";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks"; import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
import { cellPadding, headerMinHeight } from "../constants";
export default function NewVariantButton() { export default function NewVariantButton() {
const experiment = useExperiment(); const experiment = useExperiment();
@@ -21,16 +22,19 @@ export default function NewVariantButton() {
<Button <Button
w="100%" w="100%"
borderRadius={0} borderRadius={0}
alignItems="flex-start" alignItems="center"
justifyContent="center" justifyContent="center"
fontWeight="normal" fontWeight="normal"
bgColor="blue.100" bgColor="transparent"
_hover={{ bgColor: "blue.200" }} _hover={{ bgColor: "gray.100" }}
py={2} px={cellPadding.x}
px={0} // py={cellPadding.y}
onClick={onClick} onClick={onClick}
height="unset"
minH={headerMinHeight}
> >
<BsPlus size={24} /> <BsPlus size={24} />
New Variant
</Button> </Button>
</Tooltip> </Tooltip>
); );

View File

@@ -3,6 +3,13 @@ import { PromptVariant, Scenario } from "./types";
import { Center, Spinner, Text } from "@chakra-ui/react"; import { Center, Spinner, Text } from "@chakra-ui/react";
import { useExperiment } from "~/utils/hooks"; import { useExperiment } from "~/utils/hooks";
import { JSONSerializable } from "~/server/types"; 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({ export default function OutputCell({
scenario, scenario,
@@ -36,23 +43,21 @@ export default function OutputCell({
if (disabledReason) if (disabledReason)
return ( return (
<Center h="100%"> <CellShell>
<Text color="gray.500">{disabledReason}</Text> <Text color="gray.500">{disabledReason}</Text>
</Center> </CellShell>
); );
if (output.isLoading) if (output.isLoading)
return ( return (
<Center h="100%"> <CellShell>
<Spinner /> <Spinner />
</Center> </CellShell>
); );
if (!output.data) return <Center h="100%">No output</Center>; if (!output.data) return <CellShell>No output</CellShell>;
return ( return (
<Center h="100%"> <CellShell>{JSON.stringify(output.data.output.choices[0].message.content, null, 2)}</CellShell>
{JSON.stringify(output.data.output.choices[0].message.content, null, 2)}
</Center>
); );
} }

View File

@@ -6,8 +6,9 @@ import { useState } from "react";
import ResizeTextarea from "react-textarea-autosize"; import ResizeTextarea from "react-textarea-autosize";
import { Box, Button, Flex, HStack, Stack, Textarea } from "@chakra-ui/react"; 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 savedValues = scenario.variableValues as Record<string, string>;
const utils = api.useContext(); const utils = api.useContext();
@@ -30,7 +31,7 @@ export default function ScenarioHeader({ scenario }: { scenario: Scenario }) {
}, [mutation, values]); }, [mutation, values]);
return ( return (
<Stack> <Stack px={cellPadding.x} py={cellPadding.y}>
{variableLabels.map((key) => { {variableLabels.map((key) => {
const value = values[key] ?? ""; const value = values[key] ?? "";
const layoutDirection = value.length > 20 ? "column" : "row"; const layoutDirection = value.length > 20 ? "column" : "row";

View File

@@ -1,53 +1,66 @@
import { useCallback } from "react"; import { useRef } from "react";
import type { PromptVariant } from "./types"; import { type PromptVariant } from "./types";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { type JSONSerializable } from "~/server/types"; import { useHandledAsyncCallback } from "~/utils/hooks";
import VariantConfigEditor from "./VariantConfigEditor"; import { Button, HStack, Heading, Tooltip } from "@chakra-ui/react";
import EditableVariantLabel from "./EditableVariantLabel"; import { BsX } from "react-icons/bs";
import { Stack, useToast } from "@chakra-ui/react"; import { cellPadding, headerMinHeight } from "../constants";
export default function VariantHeader({ variant }: { variant: PromptVariant }) { export default function VariantHeader(props: { variant: PromptVariant }) {
const replaceWithConfig = api.promptVariants.replaceWithConfig.useMutation();
const utils = api.useContext(); const utils = api.useContext();
const toast = useToast();
const onSave = useCallback( const labelRef = useRef<HTMLHeadingElement | null>(null);
async (currentConfig: string) => { const updateMutation = api.promptVariants.update.useMutation();
let parsedConfig: JSONSerializable; const [onSaveLabel] = useHandledAsyncCallback(async () => {
try { const newLabel = labelRef.current?.innerText;
parsedConfig = JSON.parse(currentConfig) as JSONSerializable; if (newLabel && newLabel !== props.variant.label) {
} catch (e) { await updateMutation.mutateAsync({
toast({ id: props.variant.id,
title: "Invalid JSON", updates: { label: newLabel },
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,
}); });
}
}, [updateMutation, props.variant.id, props.variant.label]);
await utils.promptVariants.list.invalidate(); const hideMutation = api.promptVariants.hide.useMutation();
}, const [onHide] = useHandledAsyncCallback(async () => {
[variant.id, replaceWithConfig, utils.promptVariants.list, toast] await hideMutation.mutateAsync({
); id: props.variant.id,
});
await utils.promptVariants.list.invalidate();
}, [hideMutation, props.variant.id]);
return ( return (
<Stack w="100%"> <HStack spacing={4} alignItems="center" minH={headerMinHeight}>
<EditableVariantLabel variant={variant} /> <Heading
<VariantConfigEditor variant={variant} /> fontWeight="bold"
</Stack> 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>
); );
} }

View File

@@ -1,15 +1,21 @@
import { RouterOutputs, api } from "~/utils/api"; import { RouterOutputs, api } from "~/utils/api";
import { Scenario, type PromptVariant } from "./types"; import { Scenario, type PromptVariant } from "./types";
import VariantHeader from "./VariantHeader";
import OutputCell from "./OutputCell"; import OutputCell from "./OutputCell";
import ScenarioHeader from "./ScenarioHeader"; import ScenarioEditor from "./ScenarioEditor";
import React, { useState } from "react"; 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 NewScenarioButton from "./NewScenarioButton";
import NewVariantButton from "./NewVariantButton"; import NewVariantButton from "./NewVariantButton";
import EditableVariantLabel from "./EditableVariantLabel"; import VariantHeader from "./VariantHeader";
import VariantConfigEditor from "./VariantConfigEditor"; import VariantConfigEditor from "./VariantConfigEditor";
const stickyHeaderStyle: SystemStyleObject = {
position: "sticky",
top: 0,
backgroundColor: "#fff",
zIndex: 1,
};
const ScenarioRow = (props: { scenario: Scenario; variants: PromptVariant[] }) => { const ScenarioRow = (props: { scenario: Scenario; variants: PromptVariant[] }) => {
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
@@ -22,16 +28,16 @@ const ScenarioRow = (props: { scenario: Scenario; variants: PromptVariant[] }) =
<GridItem <GridItem
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
sx={isHovered ? highlightStyle : null} sx={isHovered ? highlightStyle : undefined}
> >
<ScenarioHeader scenario={props.scenario} /> <ScenarioEditor scenario={props.scenario} />
</GridItem> </GridItem>
{props.variants.map((variant) => ( {props.variants.map((variant) => (
<GridItem <GridItem
key={variant.id} key={variant.id}
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
sx={isHovered ? highlightStyle : null} sx={isHovered ? highlightStyle : undefined}
> >
<OutputCell key={variant.id} scenario={props.scenario} variant={variant} /> <OutputCell key={variant.id} scenario={props.scenario} variant={variant} />
</GridItem> </GridItem>
@@ -57,14 +63,12 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
<Grid <Grid
p={4} p={4}
display="grid" display="grid"
gridTemplateColumns={`200px repeat(${variants.data.length}, minmax(300px, 1fr)) 40px`} gridTemplateColumns={`200px repeat(${variants.data.length}, minmax(300px, 1fr)) auto`}
sx={{ sx={{
"> *": { "> *": {
borderColor: "gray.300", borderColor: "gray.300",
borderBottomWidth: 1, borderBottomWidth: 1,
borderRightWidth: 1, borderRightWidth: 1,
paddingX: 4,
paddingY: 2,
}, },
"> *:last-child": { "> *:last-child": {
borderRightWidth: 0, borderRightWidth: 0,
@@ -72,25 +76,24 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
}} }}
> >
<GridItem display="flex" alignItems="flex-end" rowSpan={2}> <GridItem display="flex" alignItems="flex-end" rowSpan={2}>
<Heading size="md" fontWeight="bold"> <Box sx={stickyHeaderStyle} flex={1}>
Scenario <Heading size="md" fontWeight="bold">
</Heading> Scenario
</Heading>
</Box>
</GridItem> </GridItem>
{variants.data.map((variant) => ( {variants.data.map((variant) => (
<GridItem <GridItem key={variant.uiId} padding={0} sx={stickyHeaderStyle}>
key={variant.uiId} <VariantHeader variant={variant} />
padding={0}
sx={{ position: "sticky", top: 0, backgroundColor: "#fff", zIndex: 1 }}
>
<EditableVariantLabel variant={variant} />
</GridItem> </GridItem>
))} ))}
<GridItem <GridItem
borderBottomWidth={0}
rowSpan={scenarios.data.length + 1} rowSpan={scenarios.data.length + 1}
padding={0} padding={0}
borderRightWidth={0} // Have to use `style` instead of emotion style props to work around css specificity issues conflicting with the "> *" selector on Grid
sx={{ position: "sticky", top: 0, backgroundColor: "#fff", zIndex: 1 }} style={{ borderRightWidth: 0, borderBottomWidth: 0 }}
sx={stickyHeaderStyle}
className="new-variant-button"
> >
<NewVariantButton /> <NewVariantButton />
</GridItem> </GridItem>

View File

@@ -0,0 +1,6 @@
export const cellPadding = {
x: 4,
y: 2,
};
export const headerMinHeight = 8;

View File

@@ -3,13 +3,17 @@ import Head from "next/head";
export default function AppNav(props: { children: React.ReactNode; title?: string }) { export default function AppNav(props: { children: React.ReactNode; title?: string }) {
return ( return (
<Flex minH="100vh" align="stretch"> <Flex minH="100vh">
<Head> <Head>
<title>{props.title ? `${props.title} | Prompt Bench` : "Prompt Bench"}</title> <title>{props.title ? `${props.title} | Prompt Bench` : "Prompt Bench"}</title>
</Head> </Head>
{/* Placeholder for now */} {/* Placeholder for now */}
<Box bgColor="gray.100" height="100vh" width="200px" /> <Box bgColor="gray.100" flexShrink={0} width="200px">
<Box flex={1}>{props.children}</Box> Nav Sidebar
</Box>
<Box flex={1} overflowX="auto" overflowY="auto" h="100vh">
{props.children}
</Box>
</Flex> </Flex>
); );
} }

View File

@@ -1,79 +0,0 @@
import styles from "./index.module.css";
import { signIn, signOut, useSession } from "next-auth/react";
import Head from "next/head";
import Link from "next/link";
import { api } from "~/utils/api";
export default function Home() {
const hello = api.example.hello.useQuery({ text: "from tRPC" });
return (
<>
<Head>
<title>Create T3 App</title>
<meta name="description" content="Generated by create-t3-app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<div className={styles.container}>
<h1 className={styles.title}>
Create <span className={styles.pinkSpan}>T3</span> App
</h1>
<div className={styles.cardRow}>
<Link
className={styles.card}
href="https://create.t3.gg/en/usage/first-steps"
target="_blank"
>
<h3 className={styles.cardTitle}>First Steps </h3>
<div className={styles.cardText}>
Just the basics - Everything you need to know to set up your database and
authentication.
</div>
</Link>
<Link
className={styles.card}
href="https://create.t3.gg/en/introduction"
target="_blank"
>
<h3 className={styles.cardTitle}>Documentation </h3>
<div className={styles.cardText}>
Learn more about Create T3 App, the libraries it uses, and how to deploy it.
</div>
</Link>
</div>
<div className={styles.showcaseContainer}>
<p className={styles.showcaseText}>
{hello.data ? hello.data.greeting : "Loading tRPC query..."}
</p>
<AuthShowcase />
</div>
</div>
</main>
</>
);
}
function AuthShowcase() {
const { data: sessionData } = useSession();
const { data: secretMessage } = api.example.getSecretMessage.useQuery(
undefined, // no input
{ enabled: sessionData?.user !== undefined }
);
return (
<div className={styles.authContainer}>
<p className={styles.showcaseText}>
{sessionData && <span>Logged in as {sessionData.user?.name}</span>}
{secretMessage && <span> - {secretMessage}</span>}
</p>
<button
className={styles.loginButton}
onClick={sessionData ? () => void signOut() : () => void signIn()}
>
{sessionData ? "Sign out" : "Sign in"}
</button>
</div>
);
}

View File

@@ -75,6 +75,23 @@ export const promptVariantsRouter = createTRPCRouter({
}); });
}), }),
hide: publicProcedure
.input(
z.object({
id: z.string(),
})
)
.mutation(async ({ input }) => {
return await prisma.promptVariant.update({
where: {
id: input.id,
},
data: {
visible: false,
},
});
}),
replaceWithConfig: publicProcedure replaceWithConfig: publicProcedure
.input( .input(
z.object({ z.object({

View File

@@ -1,11 +1,12 @@
import { extendTheme } from "@chakra-ui/react"; import { extendTheme } from "@chakra-ui/react";
import "@fontsource/poppins";
import "@fontsource/roboto"; const systemFont =
'ui-sans-serif, -apple-system, "system-ui", "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"';
const theme = extendTheme({ const theme = extendTheme({
fonts: { fonts: {
heading: "Poppins, sans-serif", heading: systemFont,
body: "Roboto, sans-serif", body: systemFont,
}, },
}); });