Continue polling VariantStats while LLM retrieval in progress, minor UI fixes (#54)

* Prevent zoom in on iOS

* Expand function return code background to fill cell

* Keep OutputStats on far right of cells

* Continue polling prompt stats while cells are retrieving from LLM

* Add comment to _document.tsx

* Fix prettier
This commit is contained in:
arcticfly
2023-07-17 18:04:38 -07:00
committed by GitHub
parent 8e7a6d3ae2
commit 4131aa67d0
6 changed files with 85 additions and 18 deletions

View File

@@ -1,6 +1,6 @@
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { type PromptVariant, type Scenario } from "../types"; import { type PromptVariant, type Scenario } from "../types";
import { Spinner, Text, Box, Center, Flex, VStack } from "@chakra-ui/react"; import { Spinner, Text, Center, VStack } from "@chakra-ui/react";
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks"; import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
import SyntaxHighlighter from "react-syntax-highlighter"; import SyntaxHighlighter from "react-syntax-highlighter";
import { docco } from "react-syntax-highlighter/dist/cjs/styles/hljs"; import { docco } from "react-syntax-highlighter/dist/cjs/styles/hljs";
@@ -55,7 +55,10 @@ export default function OutputCell({
const fetchingOutput = queryLoading || refetchingOutput; const fetchingOutput = queryLoading || refetchingOutput;
const awaitingOutput = const awaitingOutput =
!cell || cell.retrievalStatus === "PENDING" || cell.retrievalStatus === "IN_PROGRESS"; !cell ||
cell.retrievalStatus === "PENDING" ||
cell.retrievalStatus === "IN_PROGRESS" ||
refetchingOutput;
useEffect(() => setRefetchInterval(awaitingOutput ? 1000 : 0), [awaitingOutput]); useEffect(() => setRefetchInterval(awaitingOutput ? 1000 : 0), [awaitingOutput]);
const modelOutput = cell?.modelOutput; const modelOutput = cell?.modelOutput;
@@ -95,11 +98,18 @@ export default function OutputCell({
} }
return ( return (
<Box fontSize="xs" width="100%" flexWrap="wrap" overflowX="auto"> <VStack
<VStack w="full" spacing={0}> w="100%"
h="100%"
fontSize="xs"
flexWrap="wrap"
overflowX="auto"
justifyContent="space-between"
>
<VStack w="full" flex={1} spacing={0}>
<CellOptions refetchingOutput={refetchingOutput} refetchOutput={hardRefetch} /> <CellOptions refetchingOutput={refetchingOutput} refetchOutput={hardRefetch} />
<SyntaxHighlighter <SyntaxHighlighter
customStyle={{ overflowX: "unset" }} customStyle={{ overflowX: "unset", width: "100%", flex: 1 }}
language="json" language="json"
style={docco} style={docco}
lineProps={{ lineProps={{
@@ -117,7 +127,7 @@ export default function OutputCell({
</SyntaxHighlighter> </SyntaxHighlighter>
</VStack> </VStack>
<OutputStats model={variant.model} modelOutput={modelOutput} scenario={scenario} /> <OutputStats model={variant.model} modelOutput={modelOutput} scenario={scenario} />
</Box> </VStack>
); );
} }
@@ -125,7 +135,7 @@ export default function OutputCell({
message?.content ?? streamedContent ?? JSON.stringify(modelOutput?.output); message?.content ?? streamedContent ?? JSON.stringify(modelOutput?.output);
return ( return (
<Flex w="100%" h="100%" direction="column" justifyContent="space-between" whiteSpace="pre-wrap"> <VStack w="100%" h="100%" justifyContent="space-between" whiteSpace="pre-wrap">
<VStack w="full" alignItems="flex-start" spacing={0}> <VStack w="full" alignItems="flex-start" spacing={0}>
<CellOptions refetchingOutput={refetchingOutput} refetchOutput={hardRefetch} /> <CellOptions refetchingOutput={refetchingOutput} refetchOutput={hardRefetch} />
<Text>{contentToDisplay}</Text> <Text>{contentToDisplay}</Text>
@@ -133,6 +143,6 @@ export default function OutputCell({
{modelOutput && ( {modelOutput && (
<OutputStats model={variant.model} modelOutput={modelOutput} scenario={scenario} /> <OutputStats model={variant.model} modelOutput={modelOutput} scenario={scenario} />
)} )}
</Flex> </VStack>
); );
} }

View File

@@ -31,7 +31,7 @@ export const OutputStats = ({
const cost = promptCost + completionCost; const cost = promptCost + completionCost;
return ( return (
<HStack align="center" color="gray.500" fontSize="2xs" mt={{ base: 0, md: 1 }}> <HStack w="full" align="center" color="gray.500" fontSize="2xs" mt={{ base: 0, md: 1 }}>
<HStack flex={1}> <HStack flex={1}>
{modelOutput.outputEvaluation.map((evaluation) => { {modelOutput.outputEvaluation.map((evaluation) => {
const passed = evaluation.result > 0.5; const passed = evaluation.result > 0.5;

View File

@@ -1,12 +1,14 @@
import { HStack, Icon, Text, useToken } from "@chakra-ui/react"; import { HStack, Icon, Skeleton, Text, useToken } from "@chakra-ui/react";
import { type PromptVariant } from "./types"; import { type PromptVariant } from "./types";
import { cellPadding } from "../constants"; import { cellPadding } from "../constants";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import chroma from "chroma-js"; import chroma from "chroma-js";
import { BsCurrencyDollar } from "react-icons/bs"; import { BsCurrencyDollar } from "react-icons/bs";
import { CostTooltip } from "../tooltip/CostTooltip"; import { CostTooltip } from "../tooltip/CostTooltip";
import { useEffect, useState } from "react";
export default function VariantStats(props: { variant: PromptVariant }) { export default function VariantStats(props: { variant: PromptVariant }) {
const [refetchInterval, setRefetchInterval] = useState(0);
const { data } = api.promptVariants.stats.useQuery( const { data } = api.promptVariants.stats.useQuery(
{ {
variantId: props.variant.id, variantId: props.variant.id,
@@ -19,10 +21,18 @@ export default function VariantStats(props: { variant: PromptVariant }) {
completionTokens: 0, completionTokens: 0,
scenarioCount: 0, scenarioCount: 0,
outputCount: 0, outputCount: 0,
awaitingRetrievals: false,
}, },
refetchInterval,
}, },
); );
// Poll every two seconds while we are waiting for LLM retrievals to finish
useEffect(
() => setRefetchInterval(data.awaitingRetrievals ? 2000 : 0),
[data.awaitingRetrievals],
);
const [passColor, neutralColor, failColor] = useToken("colors", [ const [passColor, neutralColor, failColor] = useToken("colors", [
"green.500", "green.500",
"gray.500", "gray.500",
@@ -33,16 +43,20 @@ export default function VariantStats(props: { variant: PromptVariant }) {
const showNumFinished = data.scenarioCount > 0 && data.scenarioCount !== data.outputCount; const showNumFinished = data.scenarioCount > 0 && data.scenarioCount !== data.outputCount;
if (!(data.evalResults.length > 0) && !data.overallCost) return null;
return ( return (
<HStack justifyContent="space-between" alignItems="center" mx="2" fontSize="xs"> <HStack
justifyContent="space-between"
alignItems="center"
mx="2"
fontSize="xs"
py={cellPadding.y}
>
{showNumFinished && ( {showNumFinished && (
<Text> <Text>
{data.outputCount} / {data.scenarioCount} {data.outputCount} / {data.scenarioCount}
</Text> </Text>
)} )}
<HStack px={cellPadding.x} py={cellPadding.y}> <HStack px={cellPadding.x}>
{data.evalResults.map((result) => { {data.evalResults.map((result) => {
const passedFrac = result.passCount / result.totalCount; const passedFrac = result.passCount / result.totalCount;
return ( return (
@@ -55,17 +69,19 @@ export default function VariantStats(props: { variant: PromptVariant }) {
); );
})} })}
</HStack> </HStack>
{data.overallCost && ( {data.overallCost && !data.awaitingRetrievals ? (
<CostTooltip <CostTooltip
promptTokens={data.promptTokens} promptTokens={data.promptTokens}
completionTokens={data.completionTokens} completionTokens={data.completionTokens}
cost={data.overallCost} cost={data.overallCost}
> >
<HStack spacing={0} align="center" color="gray.500" my="2"> <HStack spacing={0} align="center" color="gray.500">
<Icon as={BsCurrencyDollar} /> <Icon as={BsCurrencyDollar} />
<Text mr={1}>{data.overallCost.toFixed(3)}</Text> <Text mr={1}>{data.overallCost.toFixed(3)}</Text>
</HStack> </HStack>
</CostTooltip> </CostTooltip>
) : (
<Skeleton height={4} width={12} mr={1} />
)} )}
</HStack> </HStack>
); );

View File

@@ -20,7 +20,6 @@ export const CostTooltip = ({
color="gray.800" color="gray.800"
bgColor="gray.50" bgColor="gray.50"
borderWidth={1} borderWidth={1}
py={2}
hasArrow hasArrow
shouldWrapChildren shouldWrapChildren
label={ label={

23
src/pages/_document.tsx Normal file
View File

@@ -0,0 +1,23 @@
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;

View File

@@ -109,7 +109,26 @@ export const promptVariantsRouter = createTRPCRouter({
const overallCost = overallPromptCost + overallCompletionCost; const overallCost = overallPromptCost + overallCompletionCost;
return { evalResults, promptTokens, completionTokens, overallCost, scenarioCount, outputCount }; const awaitingRetrievals = !!(await prisma.scenarioVariantCell.findFirst({
where: {
promptVariantId: input.variantId,
testScenario: { visible: true },
// Check if is PENDING or IN_PROGRESS
retrievalStatus: {
in: ["PENDING", "IN_PROGRESS"],
},
},
}));
return {
evalResults,
promptTokens,
completionTokens,
overallCost,
scenarioCount,
outputCount,
awaitingRetrievals,
};
}), }),
create: publicProcedure create: publicProcedure