diff --git a/.eslintrc.cjs b/.eslintrc.cjs index e68e9a7..b9e381e 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -32,6 +32,7 @@ const config = { ], "unused-imports/no-unused-imports": "error", + "@typescript-eslint/no-unused-vars": "off", "unused-imports/no-unused-vars": [ "warn", { vars: "all", varsIgnorePattern: "^_", args: "after-used", argsIgnorePattern: "^_" }, diff --git a/package.json b/package.json index 2c3d596..77401e6 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,8 @@ "socket.io-client": "^4.7.1", "superjson": "1.12.2", "tsx": "^3.12.7", - "zod": "^3.21.4" + "zod": "^3.21.4", + "zustand": "^4.3.9" }, "devDependencies": { "@openapi-contrib/openapi-schema-to-json-schema": "^4.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62c64a0..8c9ac71 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,6 +119,9 @@ dependencies: zod: specifier: ^3.21.4 version: 3.21.4 + zustand: + specifier: ^4.3.9 + version: 4.3.9(react@18.2.0) devDependencies: '@openapi-contrib/openapi-schema-to-json-schema': @@ -5964,3 +5967,19 @@ packages: /zod@3.21.4: resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} dev: false + + /zustand@4.3.9(react@18.2.0): + resolution: {integrity: sha512-Tat5r8jOMG1Vcsj8uldMyqYKC5IZvQif8zetmLHs9WoZlntTHmIoNM8TpLRY31ExncuUvUOXehd0kvahkuHjDw==} + engines: {node: '>=12.7.0'} + peerDependencies: + immer: '>=9.0' + react: '>=16.8' + peerDependenciesMeta: + immer: + optional: true + react: + optional: true + dependencies: + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false diff --git a/src/components/OutputsTable/EditEvaluations.tsx b/src/components/OutputsTable/EditEvaluations.tsx new file mode 100644 index 0000000..76e0576 --- /dev/null +++ b/src/components/OutputsTable/EditEvaluations.tsx @@ -0,0 +1,44 @@ +import { Text, Heading, Stack } from "@chakra-ui/react"; +import { useState } from "react"; +import { api } from "~/utils/api"; +import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks"; +import { useStore } from "~/utils/store"; + +export default function EditEvaluations() { + const experiment = useExperiment(); + const vars = + api.templateVars.list.useQuery({ experimentId: experiment.data?.id ?? "" }).data ?? []; + + const [newVariable, setNewVariable] = useState(""); + const newVarIsValid = newVariable.length > 0 && !vars.map((v) => v.label).includes(newVariable); + + const utils = api.useContext(); + const addVarMutation = api.templateVars.create.useMutation(); + const [onAddVar] = useHandledAsyncCallback(async () => { + if (!experiment.data?.id) return; + if (!newVarIsValid) return; + await addVarMutation.mutateAsync({ + experimentId: experiment.data.id, + label: newVariable, + }); + await utils.templateVars.list.invalidate(); + setNewVariable(""); + }, [addVarMutation, experiment.data?.id, newVarIsValid, newVariable]); + + const deleteMutation = api.templateVars.delete.useMutation(); + const [onDeleteVar] = useHandledAsyncCallback(async (id: string) => { + await deleteMutation.mutateAsync({ id }); + await utils.templateVars.list.invalidate(); + }, []); + + const closeDrawer = useStore((state) => state.closeDrawer); + + return ( + + Edit Evaluations + + + + + ); +} diff --git a/src/components/OutputsTable/EditScenarioVars.tsx b/src/components/OutputsTable/EditScenarioVars.tsx new file mode 100644 index 0000000..fb2cfe0 --- /dev/null +++ b/src/components/OutputsTable/EditScenarioVars.tsx @@ -0,0 +1,104 @@ +import { Text, Button, HStack, Heading, Icon, Input, Stack, Code } from "@chakra-ui/react"; +import { useState } from "react"; +import { BsCheck, BsX } from "react-icons/bs"; +import { api } from "~/utils/api"; +import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks"; + +export default function EditScenarioVars() { + const experiment = useExperiment(); + const vars = + api.templateVars.list.useQuery({ experimentId: experiment.data?.id ?? "" }).data ?? []; + + const [newVariable, setNewVariable] = useState(""); + const newVarIsValid = newVariable.length > 0 && !vars.map((v) => v.label).includes(newVariable); + + const utils = api.useContext(); + const addVarMutation = api.templateVars.create.useMutation(); + const [onAddVar] = useHandledAsyncCallback(async () => { + if (!experiment.data?.id) return; + if (!newVarIsValid) return; + await addVarMutation.mutateAsync({ + experimentId: experiment.data.id, + label: newVariable, + }); + await utils.templateVars.list.invalidate(); + setNewVariable(""); + }, [addVarMutation, experiment.data?.id, newVarIsValid, newVariable]); + + const deleteMutation = api.templateVars.delete.useMutation(); + const [onDeleteVar] = useHandledAsyncCallback(async (id: string) => { + await deleteMutation.mutateAsync({ id }); + await utils.templateVars.list.invalidate(); + }, []); + + return ( + + Edit Scenario Variables + + + Scenario variables can be used in your prompt variants as well as evaluations. Reference + them using {"{{curly_braces}}"}. + + + setNewVariable(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + onAddVar(); + } + // If the user types a space, replace it with an underscore + if (e.key === " ") { + e.preventDefault(); + setNewVariable((v) => v + "_"); + } + }} + /> + + + + + {vars.map((variable) => ( + + + {variable.label} + + + + ))} + + + + ); +} diff --git a/src/components/OutputsTable/OutputCell.tsx b/src/components/OutputsTable/OutputCell.tsx index a40521c..0aed4d5 100644 --- a/src/components/OutputsTable/OutputCell.tsx +++ b/src/components/OutputsTable/OutputCell.tsx @@ -114,7 +114,8 @@ export default function OutputCell({ ); } - const contentToDisplay = message?.content ?? streamedContent ?? JSON.stringify(output.data?.output); + const contentToDisplay = + message?.content ?? streamedContent ?? JSON.stringify(output.data?.output); return ( diff --git a/src/components/OutputsTable/SettingsDrawer.tsx b/src/components/OutputsTable/SettingsDrawer.tsx new file mode 100644 index 0000000..a07487a --- /dev/null +++ b/src/components/OutputsTable/SettingsDrawer.tsx @@ -0,0 +1,32 @@ +import { + Drawer, + DrawerBody, + DrawerCloseButton, + DrawerContent, + DrawerHeader, + DrawerOverlay, + Heading, +} from "@chakra-ui/react"; +import { useStore } from "~/utils/store"; +import EditScenarioVars from "./EditScenarioVars"; + +export default function SettingsDrawer() { + const isOpen = useStore((state) => state.drawerOpen); + const closeDrawer = useStore((state) => state.closeDrawer); + + return ( + + + + + + Settings + + + + {/* */} + + + + ); +} diff --git a/src/components/OutputsTable/index.tsx b/src/components/OutputsTable/index.tsx index 4b648d5..c8649db 100644 --- a/src/components/OutputsTable/index.tsx +++ b/src/components/OutputsTable/index.tsx @@ -1,11 +1,20 @@ -import { Box, Grid, GridItem, Heading, type SystemStyleObject } from "@chakra-ui/react"; -import ScenarioHeader from "~/server/ScenarioHeader"; +import { + Button, + Grid, + GridItem, + HStack, + Heading, + type SystemStyleObject, +} from "@chakra-ui/react"; import { api } from "~/utils/api"; import NewScenarioButton from "./NewScenarioButton"; import NewVariantButton from "./NewVariantButton"; import ScenarioRow from "./ScenarioRow"; import VariantConfigEditor from "./VariantConfigEditor"; import VariantHeader from "./VariantHeader"; +import { cellPadding } from "../constants"; +import { BsPencil } from "react-icons/bs"; +import { useStore } from "~/utils/store"; const stickyHeaderStyle: SystemStyleObject = { position: "sticky", @@ -19,6 +28,7 @@ export default function OutputsTable({ experimentId }: { experimentId: string | { experimentId: experimentId as string }, { enabled: !!experimentId } ); + const openDrawer = useStore((s) => s.openDrawer); const scenarios = api.scenarios.list.useQuery( { experimentId: experimentId as string }, @@ -42,8 +52,28 @@ export default function OutputsTable({ experimentId }: { experimentId: string | }, }} > - - + + + + Scenario + + + {variants.data.map((variant) => ( diff --git a/src/pages/experiments/[id].tsx b/src/pages/experiments/[id].tsx index 40589a5..0888c36 100644 --- a/src/pages/experiments/[id].tsx +++ b/src/pages/experiments/[id].tsx @@ -11,12 +11,14 @@ import { } from "@chakra-ui/react"; import { useRouter } from "next/router"; import { useState, useEffect } from "react"; -import { BsTrash } from "react-icons/bs"; +import { BsGearFill, BsTrash } from "react-icons/bs"; import { RiFlaskLine } from "react-icons/ri"; import OutputsTable from "~/components/OutputsTable"; +import SettingsDrawer from "~/components/OutputsTable/SettingsDrawer"; import AppShell from "~/components/nav/AppShell"; import { api } from "~/utils/api"; import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks"; +import { useStore } from "~/utils/store"; const DeleteButton = (props: { experimentId: string }) => { const mutation = api.experiments.delete.useMutation(); @@ -44,6 +46,7 @@ export default function Experiment() { const router = useRouter(); const experiment = useExperiment(); const utils = api.useContext(); + const openDrawer = useStore((s) => s.openDrawer); const [label, setLabel] = useState(experiment.data?.label || ""); useEffect(() => { @@ -98,8 +101,19 @@ export default function Experiment() { /> + + diff --git a/src/server/ScenarioHeader.tsx b/src/server/ScenarioHeader.tsx deleted file mode 100644 index b3a24ca..0000000 --- a/src/server/ScenarioHeader.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { Text, Button, HStack, Heading, Icon, Input, Stack, Code } from "@chakra-ui/react"; -import { useState } from "react"; -import { BsCheck, BsChevronDown, BsX } from "react-icons/bs"; -import { cellPadding } from "~/components/constants"; -import { api } from "~/utils/api"; -import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks"; - -export default function ScenarioHeader() { - const experiment = useExperiment(); - const vars = - api.templateVars.list.useQuery({ experimentId: experiment.data?.id ?? "" }).data ?? []; - - const [newVariable, setNewVariable] = useState(""); - const newVarIsValid = newVariable.length > 0 && !vars.map((v) => v.label).includes(newVariable); - - const [editing, setEditing] = useState(false); - - const utils = api.useContext(); - const addVarMutation = api.templateVars.create.useMutation(); - const [onAddVar] = useHandledAsyncCallback(async () => { - if (!experiment.data?.id) return; - if (!newVarIsValid) return; - await addVarMutation.mutateAsync({ - experimentId: experiment.data.id, - label: newVariable, - }); - await utils.templateVars.list.invalidate(); - setNewVariable(""); - }, [addVarMutation, experiment.data?.id, newVarIsValid, newVariable]); - - const deleteMutation = api.templateVars.delete.useMutation(); - const [onDeleteVar] = useHandledAsyncCallback(async (id: string) => { - await deleteMutation.mutateAsync({ id }); - await utils.templateVars.list.invalidate(); - }, []); - - return ( - - - - Scenario - - { - - } - - {editing && ( - - - Define scenario variables. Reference them from your prompt variants using{" "} - {`{{curly_braces}}`}. - - - setNewVariable(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - onAddVar(); - } - // If the user types a space, replace it with an underscore - if (e.key === " ") { - e.preventDefault(); - setNewVariable((v) => v + "_"); - } - }} - /> - - - - - {vars.map((variable) => ( - - - {variable.label} - - - - ))} - - - )} - - ); -} diff --git a/src/utils/store.ts b/src/utils/store.ts new file mode 100644 index 0000000..462c486 --- /dev/null +++ b/src/utils/store.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +type StoreState = { + drawerOpen: boolean; + openDrawer: () => void; + closeDrawer: () => void; +}; + +export const useStore = create()((set) => ({ + drawerOpen: true, + openDrawer: () => set({ drawerOpen: true }), + closeDrawer: () => set({ drawerOpen: false }), +}));