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
This commit is contained in:
48
src/components/OutputsTable/ScenariosHeader.tsx
Normal file
48
src/components/OutputsTable/ScenariosHeader.tsx
Normal file
@@ -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 (
|
||||
<GridItem
|
||||
ref={ref as any}
|
||||
display="flex"
|
||||
alignItems="flex-end"
|
||||
rowSpan={headerRows}
|
||||
px={cellPadding.x}
|
||||
py={cellPadding.y}
|
||||
// Only display the part of the grid item that has content
|
||||
sx={{ ...stickyHeaderStyle, top: topValue }}
|
||||
>
|
||||
<HStack w="100%">
|
||||
<Heading size="xs" fontWeight="bold" flex={1}>
|
||||
Scenarios ({numScenarios})
|
||||
</Heading>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color="gray.500"
|
||||
aria-label="Edit"
|
||||
leftIcon={<BsPencil />}
|
||||
onClick={openDrawer}
|
||||
>
|
||||
Edit Vars
|
||||
</Button>
|
||||
</HStack>
|
||||
</GridItem>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
</Button>
|
||||
<Tooltip label={`${modifierKey} + Enter`}>
|
||||
<Button size="sm" onClick={onSave} colorScheme="blue">
|
||||
Save
|
||||
<Button size="sm" onClick={onSave} colorScheme="blue" w={16} disabled={saveInProgress}>
|
||||
{saveInProgress ? <Spinner boxSize={4} /> : <Text>Save</Text>}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<GridItem
|
||||
display="flex"
|
||||
alignItems="flex-end"
|
||||
rowSpan={headerRows}
|
||||
px={cellPadding.x}
|
||||
py={cellPadding.y}
|
||||
// TODO: This is a hack to get the sticky header to work. It's not ideal because it's not responsive to the height of the header,
|
||||
// so if the header height changes, this will need to be updated.
|
||||
sx={{ ...stickyHeaderStyle, top: "-337px" }}
|
||||
>
|
||||
<HStack w="100%">
|
||||
<Heading size="xs" fontWeight="bold" flex={1}>
|
||||
Scenarios ({scenarios.data.length})
|
||||
</Heading>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color="gray.500"
|
||||
aria-label="Edit"
|
||||
leftIcon={<BsPencil />}
|
||||
onClick={openDrawer}
|
||||
>
|
||||
Edit Vars
|
||||
</Button>
|
||||
</HStack>
|
||||
</GridItem>
|
||||
<ScenariosHeader headerRows={headerRows} numScenarios={scenarios.data.length} />
|
||||
|
||||
{variants.data.map((variant) => (
|
||||
<GridItem key={variant.uiId} padding={0} sx={stickyHeaderStyle} borderTopWidth={1}>
|
||||
|
||||
8
src/components/OutputsTable/styles.ts
Normal file
8
src/components/OutputsTable/styles.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { type SystemStyleObject } from "@chakra-ui/react";
|
||||
|
||||
export const stickyHeaderStyle: SystemStyleObject = {
|
||||
position: "sticky",
|
||||
top: "-1px",
|
||||
backgroundColor: "#fff",
|
||||
zIndex: 1,
|
||||
};
|
||||
@@ -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 (
|
||||
<>
|
||||
<Head>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"
|
||||
/>
|
||||
</Head>
|
||||
<SessionProvider session={session}>
|
||||
<Favicon />
|
||||
<ChakraProvider theme={theme}>
|
||||
<Component {...pageProps} />
|
||||
</ChakraProvider>
|
||||
</SessionProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import Document, { Html, Head, Main, NextScript } from "next/document";
|
||||
|
||||
class MyDocument extends Document {
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
{/* Prevent automatic zoom-in on iPhone when focusing on text input */}
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"
|
||||
/>
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument;
|
||||
@@ -2,6 +2,16 @@ import { type JSONSerializable } from "../types";
|
||||
|
||||
export type VariableMap = Record<string, string>;
|
||||
|
||||
// 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] || "");
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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<HTMLElement>, Dimensions | undefined] => {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const [dimensions, setDimensions] = useState<Dimensions | undefined>();
|
||||
|
||||
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];
|
||||
};
|
||||
Reference in New Issue
Block a user