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 { useRef, useEffect, useState, useCallback } from "react";
|
||||||
import { useHandledAsyncCallback, useModifierKeyLabel } from "~/utils/hooks";
|
import { useHandledAsyncCallback, useModifierKeyLabel } from "~/utils/hooks";
|
||||||
import { type PromptVariant } from "./types";
|
import { type PromptVariant } from "./types";
|
||||||
@@ -27,7 +27,7 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
|||||||
const utils = api.useContext();
|
const utils = api.useContext();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const [onSave] = useHandledAsyncCallback(async () => {
|
const [onSave, saveInProgress] = useHandledAsyncCallback(async () => {
|
||||||
if (!editorRef.current) return;
|
if (!editorRef.current) return;
|
||||||
|
|
||||||
await editorRef.current.getAction("editor.action.formatDocument")?.run();
|
await editorRef.current.getAction("editor.action.formatDocument")?.run();
|
||||||
@@ -146,8 +146,8 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
|||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
<Tooltip label={`${modifierKey} + Enter`}>
|
<Tooltip label={`${modifierKey} + Enter`}>
|
||||||
<Button size="sm" onClick={onSave} colorScheme="blue">
|
<Button size="sm" onClick={onSave} colorScheme="blue" w={16} disabled={saveInProgress}>
|
||||||
Save
|
{saveInProgress ? <Spinner boxSize={4} /> : <Text>Save</Text>}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</HStack>
|
</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 { api } from "~/utils/api";
|
||||||
import NewScenarioButton from "./NewScenarioButton";
|
import NewScenarioButton from "./NewScenarioButton";
|
||||||
import NewVariantButton from "./NewVariantButton";
|
import NewVariantButton from "./NewVariantButton";
|
||||||
import ScenarioRow from "./ScenarioRow";
|
import ScenarioRow from "./ScenarioRow";
|
||||||
import VariantEditor from "./VariantEditor";
|
import VariantEditor from "./VariantEditor";
|
||||||
import VariantHeader from "./VariantHeader";
|
import VariantHeader from "./VariantHeader";
|
||||||
import { cellPadding } from "../constants";
|
|
||||||
import { BsPencil } from "react-icons/bs";
|
|
||||||
import VariantStats from "./VariantStats";
|
import VariantStats from "./VariantStats";
|
||||||
import { useAppStore } from "~/state/store";
|
import { ScenariosHeader } from "./ScenariosHeader";
|
||||||
|
import { stickyHeaderStyle } from "./styles";
|
||||||
const stickyHeaderStyle: SystemStyleObject = {
|
|
||||||
position: "sticky",
|
|
||||||
top: "-1px",
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
zIndex: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function OutputsTable({ experimentId }: { experimentId: string | undefined }) {
|
export default function OutputsTable({ experimentId }: { experimentId: string | undefined }) {
|
||||||
const variants = api.promptVariants.list.useQuery(
|
const variants = api.promptVariants.list.useQuery(
|
||||||
{ experimentId: experimentId as string },
|
{ experimentId: experimentId as string },
|
||||||
{ enabled: !!experimentId },
|
{ enabled: !!experimentId },
|
||||||
);
|
);
|
||||||
const openDrawer = useAppStore((s) => s.openDrawer);
|
|
||||||
|
|
||||||
const scenarios = api.scenarios.list.useQuery(
|
const scenarios = api.scenarios.list.useQuery(
|
||||||
{ experimentId: experimentId as string },
|
{ experimentId: experimentId as string },
|
||||||
@@ -49,32 +40,7 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
|||||||
}}
|
}}
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
>
|
>
|
||||||
<GridItem
|
<ScenariosHeader headerRows={headerRows} numScenarios={scenarios.data.length} />
|
||||||
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>
|
|
||||||
|
|
||||||
{variants.data.map((variant) => (
|
{variants.data.map((variant) => (
|
||||||
<GridItem key={variant.uiId} padding={0} sx={stickyHeaderStyle} borderTopWidth={1}>
|
<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 theme from "~/utils/theme";
|
||||||
import Favicon from "~/components/Favicon";
|
import Favicon from "~/components/Favicon";
|
||||||
import "~/utils/analytics";
|
import "~/utils/analytics";
|
||||||
|
import Head from "next/head";
|
||||||
|
|
||||||
const MyApp: AppType<{ session: Session | null }> = ({
|
const MyApp: AppType<{ session: Session | null }> = ({
|
||||||
Component,
|
Component,
|
||||||
pageProps: { session, ...pageProps },
|
pageProps: { session, ...pageProps },
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
<SessionProvider session={session}>
|
<SessionProvider session={session}>
|
||||||
<Favicon />
|
<Favicon />
|
||||||
<ChakraProvider theme={theme}>
|
<ChakraProvider theme={theme}>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
</SessionProvider>
|
</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>;
|
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 {
|
export function fillTemplate(template: string, variables: VariableMap): string {
|
||||||
return template.replace(/{{\s*(\w+)\s*}}/g, (_, key: string) => variables[key] || "");
|
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 Evaluation, type ModelOutput, type TestScenario } from "@prisma/client";
|
||||||
import { type ChatCompletion } from "openai/resources/chat";
|
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 { openai } from "./openai";
|
||||||
import dedent from "dedent";
|
import dedent from "dedent";
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ export const runOneEval = async (
|
|||||||
|
|
||||||
const stringifiedMessage = message.content ?? JSON.stringify(message.function_call);
|
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) {
|
switch (evaluation.evalType) {
|
||||||
case "CONTAINS":
|
case "CONTAINS":
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useRouter } from "next/router";
|
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";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
export const useExperiment = () => {
|
export const useExperiment = () => {
|
||||||
@@ -49,3 +49,41 @@ export const useModifierKeyLabel = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
return label;
|
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