From 374d0237eec24095e7fbbe69a1e6b90b81709402 Mon Sep 17 00:00:00 2001 From: arcticfly <41524992+arcticfly@users.noreply.github.com> Date: Tue, 18 Jul 2023 11:07:04 -0700 Subject: [PATCH] Escape characters in Regex evaluations, minor UI fixes (#56) * Fix ScenariosHeader stickiness * Move meta tag from _app.tsx to _document.tsx * Show spinner when saving variant * Escape quotes and regex in evaluations --- .../OutputsTable/ScenariosHeader.tsx | 48 +++++++++++++++++++ src/components/OutputsTable/VariantEditor.tsx | 8 ++-- src/components/OutputsTable/index.tsx | 42 ++-------------- src/components/OutputsTable/styles.ts | 8 ++++ src/pages/_app.tsx | 21 +++++--- src/pages/_document.tsx | 23 --------- src/server/utils/fillTemplate.ts | 10 ++++ src/server/utils/runOneEval.ts | 4 +- src/utils/hooks.ts | 40 +++++++++++++++- 9 files changed, 130 insertions(+), 74 deletions(-) create mode 100644 src/components/OutputsTable/ScenariosHeader.tsx create mode 100644 src/components/OutputsTable/styles.ts delete mode 100644 src/pages/_document.tsx diff --git a/src/components/OutputsTable/ScenariosHeader.tsx b/src/components/OutputsTable/ScenariosHeader.tsx new file mode 100644 index 0000000..9a776be --- /dev/null +++ b/src/components/OutputsTable/ScenariosHeader.tsx @@ -0,0 +1,48 @@ +import { Button, GridItem, HStack, Heading } from "@chakra-ui/react"; +import { cellPadding } from "../constants"; +import { useElementDimensions } from "~/utils/hooks"; +import { stickyHeaderStyle } from "./styles"; +import { BsPencil } from "react-icons/bs"; +import { useAppStore } from "~/state/store"; + +export const ScenariosHeader = ({ + headerRows, + numScenarios, +}: { + headerRows: number; + numScenarios: number; +}) => { + const openDrawer = useAppStore((s) => s.openDrawer); + + const [ref, dimensions] = useElementDimensions(); + const topValue = dimensions ? `-${dimensions.height - 24}px` : "-455px"; + + return ( + + + + Scenarios ({numScenarios}) + + + + + ); +}; diff --git a/src/components/OutputsTable/VariantEditor.tsx b/src/components/OutputsTable/VariantEditor.tsx index 14509c2..d5102ba 100644 --- a/src/components/OutputsTable/VariantEditor.tsx +++ b/src/components/OutputsTable/VariantEditor.tsx @@ -1,4 +1,4 @@ -import { Box, Button, HStack, Tooltip, useToast } from "@chakra-ui/react"; +import { Box, Button, HStack, Spinner, Tooltip, useToast, Text } from "@chakra-ui/react"; import { useRef, useEffect, useState, useCallback } from "react"; import { useHandledAsyncCallback, useModifierKeyLabel } from "~/utils/hooks"; import { type PromptVariant } from "./types"; @@ -27,7 +27,7 @@ export default function VariantEditor(props: { variant: PromptVariant }) { const utils = api.useContext(); const toast = useToast(); - const [onSave] = useHandledAsyncCallback(async () => { + const [onSave, saveInProgress] = useHandledAsyncCallback(async () => { if (!editorRef.current) return; await editorRef.current.getAction("editor.action.formatDocument")?.run(); @@ -146,8 +146,8 @@ export default function VariantEditor(props: { variant: PromptVariant }) { Reset - diff --git a/src/components/OutputsTable/index.tsx b/src/components/OutputsTable/index.tsx index db38e88..782fe4e 100644 --- a/src/components/OutputsTable/index.tsx +++ b/src/components/OutputsTable/index.tsx @@ -1,28 +1,19 @@ -import { Button, Grid, GridItem, HStack, Heading, type SystemStyleObject } from "@chakra-ui/react"; +import { Grid, GridItem } from "@chakra-ui/react"; import { api } from "~/utils/api"; import NewScenarioButton from "./NewScenarioButton"; import NewVariantButton from "./NewVariantButton"; import ScenarioRow from "./ScenarioRow"; import VariantEditor from "./VariantEditor"; import VariantHeader from "./VariantHeader"; -import { cellPadding } from "../constants"; -import { BsPencil } from "react-icons/bs"; import VariantStats from "./VariantStats"; -import { useAppStore } from "~/state/store"; - -const stickyHeaderStyle: SystemStyleObject = { - position: "sticky", - top: "-1px", - backgroundColor: "#fff", - zIndex: 1, -}; +import { ScenariosHeader } from "./ScenariosHeader"; +import { stickyHeaderStyle } from "./styles"; export default function OutputsTable({ experimentId }: { experimentId: string | undefined }) { const variants = api.promptVariants.list.useQuery( { experimentId: experimentId as string }, { enabled: !!experimentId }, ); - const openDrawer = useAppStore((s) => s.openDrawer); const scenarios = api.scenarios.list.useQuery( { experimentId: experimentId as string }, @@ -49,32 +40,7 @@ export default function OutputsTable({ experimentId }: { experimentId: string | }} fontSize="sm" > - - - - Scenarios ({scenarios.data.length}) - - - - + {variants.data.map((variant) => ( diff --git a/src/components/OutputsTable/styles.ts b/src/components/OutputsTable/styles.ts new file mode 100644 index 0000000..0dc8dc8 --- /dev/null +++ b/src/components/OutputsTable/styles.ts @@ -0,0 +1,8 @@ +import { type SystemStyleObject } from "@chakra-ui/react"; + +export const stickyHeaderStyle: SystemStyleObject = { + position: "sticky", + top: "-1px", + backgroundColor: "#fff", + zIndex: 1, +}; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 0f371e9..6ec8391 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -6,18 +6,27 @@ import { ChakraProvider } from "@chakra-ui/react"; import theme from "~/utils/theme"; import Favicon from "~/components/Favicon"; import "~/utils/analytics"; +import Head from "next/head"; const MyApp: AppType<{ session: Session | null }> = ({ Component, pageProps: { session, ...pageProps }, }) => { return ( - - - - - - + <> + + + + + + + + + + ); }; diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx deleted file mode 100644 index 4c5f82d..0000000 --- a/src/pages/_document.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import Document, { Html, Head, Main, NextScript } from "next/document"; - -class MyDocument extends Document { - render() { - return ( - - - {/* Prevent automatic zoom-in on iPhone when focusing on text input */} - - - -
- - - - ); - } -} - -export default MyDocument; diff --git a/src/server/utils/fillTemplate.ts b/src/server/utils/fillTemplate.ts index 955b10a..033560a 100644 --- a/src/server/utils/fillTemplate.ts +++ b/src/server/utils/fillTemplate.ts @@ -2,6 +2,16 @@ import { type JSONSerializable } from "../types"; export type VariableMap = Record; +// Escape quotes to match the way we encode JSON +export function escapeQuotes(str: string) { + return str.replace(/(\\")|"/g, (match, p1) => (p1 ? match : '\\"')); +} + +// Escape regex special characters +export function escapeRegExp(str: string) { + return str.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + export function fillTemplate(template: string, variables: VariableMap): string { return template.replace(/{{\s*(\w+)\s*}}/g, (_, key: string) => variables[key] || ""); } diff --git a/src/server/utils/runOneEval.ts b/src/server/utils/runOneEval.ts index 54c8960..6a472bc 100644 --- a/src/server/utils/runOneEval.ts +++ b/src/server/utils/runOneEval.ts @@ -1,6 +1,6 @@ import { type Evaluation, type ModelOutput, type TestScenario } from "@prisma/client"; import { type ChatCompletion } from "openai/resources/chat"; -import { type VariableMap, fillTemplate } from "./fillTemplate"; +import { type VariableMap, fillTemplate, escapeRegExp, escapeQuotes } from "./fillTemplate"; import { openai } from "./openai"; import dedent from "dedent"; @@ -80,7 +80,7 @@ export const runOneEval = async ( const stringifiedMessage = message.content ?? JSON.stringify(message.function_call); - const matchRegex = fillTemplate(evaluation.value, scenario.variableValues as VariableMap); + const matchRegex = escapeRegExp(fillTemplate(escapeQuotes(evaluation.value), scenario.variableValues as VariableMap)); switch (evaluation.evalType) { case "CONTAINS": diff --git a/src/utils/hooks.ts b/src/utils/hooks.ts index 00b559e..d726112 100644 --- a/src/utils/hooks.ts +++ b/src/utils/hooks.ts @@ -1,5 +1,5 @@ import { useRouter } from "next/router"; -import { useCallback, useEffect, useState } from "react"; +import { type RefObject, useCallback, useEffect, useRef, useState } from "react"; import { api } from "~/utils/api"; export const useExperiment = () => { @@ -49,3 +49,41 @@ export const useModifierKeyLabel = () => { }, []); return label; }; + +interface Dimensions { + left: number; + top: number; + right: number; + bottom: number; + width: number; + height: number; + x: number; + y: number; +} + +// get dimensions of an element +export const useElementDimensions = (): [RefObject, Dimensions | undefined] => { + const ref = useRef(null); + const [dimensions, setDimensions] = useState(); + + useEffect(() => { + if (ref.current) { + const observer = new ResizeObserver(entries => { + entries.forEach(entry => { + setDimensions(entry.contentRect); + }); + }); + + observer.observe(ref.current); + + // Cleanup the observer on component unmount + return () => { + if (ref.current) { + observer.unobserve(ref.current); + } + } + } + }, []); + + return [ref, dimensions]; +}; \ No newline at end of file