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})
+
+ }
+ onClick={openDrawer}
+ >
+ Edit Vars
+
+
+
+ );
+};
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})
-
- }
- onClick={openDrawer}
- >
- Edit Vars
-
-
-
+
{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