Compare commits
1 Commits
space-out-
...
fullscreen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc1d1178da |
@@ -1,48 +0,0 @@
|
|||||||
import { Box, Flex, Icon, Spinner } from "@chakra-ui/react";
|
|
||||||
import { BsPlus } from "react-icons/bs";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { useExperiment, useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
|
||||||
import { cellPadding } from "../constants";
|
|
||||||
import { ActionButton } from "./ScenariosHeader";
|
|
||||||
|
|
||||||
export default function AddVariantButton() {
|
|
||||||
const experiment = useExperiment();
|
|
||||||
const mutation = api.promptVariants.create.useMutation();
|
|
||||||
const utils = api.useContext();
|
|
||||||
|
|
||||||
const [onClick, loading] = useHandledAsyncCallback(async () => {
|
|
||||||
if (!experiment.data) return;
|
|
||||||
await mutation.mutateAsync({
|
|
||||||
experimentId: experiment.data.id,
|
|
||||||
});
|
|
||||||
await utils.promptVariants.list.invalidate();
|
|
||||||
}, [mutation]);
|
|
||||||
|
|
||||||
const { canModify } = useExperimentAccess();
|
|
||||||
if (!canModify) return <Box w={cellPadding.x} />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex w="100%" justifyContent="flex-end">
|
|
||||||
<ActionButton
|
|
||||||
onClick={onClick}
|
|
||||||
leftIcon={<Icon as={loading ? Spinner : BsPlus} boxSize={6} mr={loading ? 1 : 0} />}
|
|
||||||
>
|
|
||||||
Add Variant
|
|
||||||
</ActionButton>
|
|
||||||
{/* <Button
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
fontWeight="normal"
|
|
||||||
bgColor="transparent"
|
|
||||||
_hover={{ bgColor: "gray.100" }}
|
|
||||||
px={cellPadding.x}
|
|
||||||
onClick={onClick}
|
|
||||||
height="unset"
|
|
||||||
minH={headerMinHeight}
|
|
||||||
>
|
|
||||||
<Icon as={loading ? Spinner : BsPlus} boxSize={6} mr={loading ? 1 : 0} />
|
|
||||||
<Text display={{ base: "none", md: "flex" }}>Add Variant</Text>
|
|
||||||
</Button> */}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
61
src/components/OutputsTable/NewScenarioButton.tsx
Normal file
61
src/components/OutputsTable/NewScenarioButton.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Button, type ButtonProps, HStack, Spinner, Icon } from "@chakra-ui/react";
|
||||||
|
import { BsPlus } from "react-icons/bs";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { useExperiment, useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
|
|
||||||
|
// Extracted Button styling into reusable component
|
||||||
|
const StyledButton = ({ children, onClick }: ButtonProps) => (
|
||||||
|
<Button
|
||||||
|
fontWeight="normal"
|
||||||
|
bgColor="transparent"
|
||||||
|
_hover={{ bgColor: "gray.100" }}
|
||||||
|
px={2}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function NewScenarioButton() {
|
||||||
|
const { canModify } = useExperimentAccess();
|
||||||
|
|
||||||
|
const experiment = useExperiment();
|
||||||
|
const mutation = api.scenarios.create.useMutation();
|
||||||
|
const utils = api.useContext();
|
||||||
|
|
||||||
|
const [onClick] = useHandledAsyncCallback(async () => {
|
||||||
|
if (!experiment.data) return;
|
||||||
|
await mutation.mutateAsync({
|
||||||
|
experimentId: experiment.data.id,
|
||||||
|
});
|
||||||
|
await utils.scenarios.list.invalidate();
|
||||||
|
}, [mutation]);
|
||||||
|
|
||||||
|
const [onAutogenerate, autogenerating] = useHandledAsyncCallback(async () => {
|
||||||
|
if (!experiment.data) return;
|
||||||
|
await mutation.mutateAsync({
|
||||||
|
experimentId: experiment.data.id,
|
||||||
|
autogenerate: true,
|
||||||
|
});
|
||||||
|
await utils.scenarios.list.invalidate();
|
||||||
|
}, [mutation]);
|
||||||
|
|
||||||
|
if (!canModify) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<StyledButton onClick={onClick}>
|
||||||
|
<Icon as={BsPlus} boxSize={6} />
|
||||||
|
Add Scenario
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton onClick={onAutogenerate}>
|
||||||
|
<Icon
|
||||||
|
as={autogenerating ? Spinner : BsPlus}
|
||||||
|
boxSize={autogenerating ? 4 : 6}
|
||||||
|
mr={autogenerating ? 2 : 0}
|
||||||
|
/>
|
||||||
|
Autogenerate Scenario
|
||||||
|
</StyledButton>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
src/components/OutputsTable/NewVariantButton.tsx
Normal file
40
src/components/OutputsTable/NewVariantButton.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Box, Button, Icon, Spinner, Text } from "@chakra-ui/react";
|
||||||
|
import { BsPlus } from "react-icons/bs";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { useExperiment, useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
|
import { cellPadding, headerMinHeight } from "../constants";
|
||||||
|
|
||||||
|
export default function NewVariantButton() {
|
||||||
|
const experiment = useExperiment();
|
||||||
|
const mutation = api.promptVariants.create.useMutation();
|
||||||
|
const utils = api.useContext();
|
||||||
|
|
||||||
|
const [onClick, loading] = useHandledAsyncCallback(async () => {
|
||||||
|
if (!experiment.data) return;
|
||||||
|
await mutation.mutateAsync({
|
||||||
|
experimentId: experiment.data.id,
|
||||||
|
});
|
||||||
|
await utils.promptVariants.list.invalidate();
|
||||||
|
}, [mutation]);
|
||||||
|
|
||||||
|
const { canModify } = useExperimentAccess();
|
||||||
|
if (!canModify) return <Box w={cellPadding.x} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
w="100%"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
fontWeight="normal"
|
||||||
|
bgColor="transparent"
|
||||||
|
_hover={{ bgColor: "gray.100" }}
|
||||||
|
px={cellPadding.x}
|
||||||
|
onClick={onClick}
|
||||||
|
height="unset"
|
||||||
|
minH={headerMinHeight}
|
||||||
|
>
|
||||||
|
<Icon as={loading ? Spinner : BsPlus} boxSize={6} mr={loading ? 1 : 0} />
|
||||||
|
<Text display={{ base: "none", md: "flex" }}>Add Variant</Text>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,13 +4,11 @@ import { cellPadding } from "../constants";
|
|||||||
import OutputCell from "./OutputCell/OutputCell";
|
import OutputCell from "./OutputCell/OutputCell";
|
||||||
import ScenarioEditor from "./ScenarioEditor";
|
import ScenarioEditor from "./ScenarioEditor";
|
||||||
import type { PromptVariant, Scenario } from "./types";
|
import type { PromptVariant, Scenario } from "./types";
|
||||||
import { borders } from "./styles";
|
|
||||||
|
|
||||||
const ScenarioRow = (props: {
|
const ScenarioRow = (props: {
|
||||||
scenario: Scenario;
|
scenario: Scenario;
|
||||||
variants: PromptVariant[];
|
variants: PromptVariant[];
|
||||||
canHide: boolean;
|
canHide: boolean;
|
||||||
rowStart: number;
|
|
||||||
}) => {
|
}) => {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
@@ -23,21 +21,15 @@ const ScenarioRow = (props: {
|
|||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
sx={isHovered ? highlightStyle : undefined}
|
sx={isHovered ? highlightStyle : undefined}
|
||||||
borderLeftWidth={1}
|
borderLeftWidth={1}
|
||||||
{...borders}
|
|
||||||
rowStart={props.rowStart}
|
|
||||||
colStart={1}
|
|
||||||
>
|
>
|
||||||
<ScenarioEditor scenario={props.scenario} hovered={isHovered} canHide={props.canHide} />
|
<ScenarioEditor scenario={props.scenario} hovered={isHovered} canHide={props.canHide} />
|
||||||
</GridItem>
|
</GridItem>
|
||||||
{props.variants.map((variant, i) => (
|
{props.variants.map((variant) => (
|
||||||
<GridItem
|
<GridItem
|
||||||
key={variant.id}
|
key={variant.id}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
sx={isHovered ? highlightStyle : undefined}
|
sx={isHovered ? highlightStyle : undefined}
|
||||||
rowStart={props.rowStart}
|
|
||||||
colStart={i + 2}
|
|
||||||
{...borders}
|
|
||||||
>
|
>
|
||||||
<Box h="100%" w="100%" px={cellPadding.x} py={cellPadding.y}>
|
<Box h="100%" w="100%" px={cellPadding.x} py={cellPadding.y}>
|
||||||
<OutputCell key={variant.id} scenario={props.scenario} variant={variant} />
|
<OutputCell key={variant.id} scenario={props.scenario} variant={variant} />
|
||||||
|
|||||||
@@ -1,73 +1,52 @@
|
|||||||
import {
|
import { Button, GridItem, HStack, Heading } from "@chakra-ui/react";
|
||||||
Button,
|
|
||||||
type ButtonProps,
|
|
||||||
HStack,
|
|
||||||
Text,
|
|
||||||
Icon,
|
|
||||||
Menu,
|
|
||||||
MenuButton,
|
|
||||||
MenuList,
|
|
||||||
MenuItem,
|
|
||||||
IconButton,
|
|
||||||
Spinner,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { cellPadding } from "../constants";
|
import { cellPadding } from "../constants";
|
||||||
import { useExperiment, useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
import { useElementDimensions, useExperimentAccess } from "~/utils/hooks";
|
||||||
import { BsGear, BsPencil, BsPlus, BsStars } from "react-icons/bs";
|
import { stickyHeaderStyle } from "./styles";
|
||||||
|
import { BsPencil } from "react-icons/bs";
|
||||||
import { useAppStore } from "~/state/store";
|
import { useAppStore } from "~/state/store";
|
||||||
import { api } from "~/utils/api";
|
|
||||||
|
|
||||||
export const ActionButton = (props: ButtonProps) => (
|
export const ScenariosHeader = ({
|
||||||
<Button size="sm" variant="ghost" color="gray.600" {...props} />
|
headerRows,
|
||||||
);
|
numScenarios,
|
||||||
|
}: {
|
||||||
export const ScenariosHeader = (props: { numScenarios: number }) => {
|
headerRows: number;
|
||||||
|
numScenarios: number;
|
||||||
|
}) => {
|
||||||
const openDrawer = useAppStore((s) => s.openDrawer);
|
const openDrawer = useAppStore((s) => s.openDrawer);
|
||||||
const { canModify } = useExperimentAccess();
|
const { canModify } = useExperimentAccess();
|
||||||
|
|
||||||
const experiment = useExperiment();
|
const [ref, dimensions] = useElementDimensions();
|
||||||
const createScenarioMutation = api.scenarios.create.useMutation();
|
const topValue = dimensions ? `-${dimensions.height - 24}px` : "-455px";
|
||||||
const utils = api.useContext();
|
|
||||||
|
|
||||||
const [onAddScenario, loading] = useHandledAsyncCallback(
|
|
||||||
async (autogenerate: boolean) => {
|
|
||||||
if (!experiment.data) return;
|
|
||||||
await createScenarioMutation.mutateAsync({
|
|
||||||
experimentId: experiment.data.id,
|
|
||||||
autogenerate,
|
|
||||||
});
|
|
||||||
await utils.scenarios.list.invalidate();
|
|
||||||
},
|
|
||||||
[createScenarioMutation],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack w="100%" pb={cellPadding.y} pt={0} align="center" spacing={0}>
|
<GridItem
|
||||||
<Text fontSize={16} fontWeight="bold">
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
Scenarios ({props.numScenarios})
|
ref={ref as any}
|
||||||
</Text>
|
display="flex"
|
||||||
{canModify && (
|
alignItems="flex-end"
|
||||||
<Menu>
|
rowSpan={headerRows}
|
||||||
<MenuButton mt={1}>
|
px={cellPadding.x}
|
||||||
<IconButton
|
py={cellPadding.y}
|
||||||
variant="ghost"
|
// Only display the part of the grid item that has content
|
||||||
aria-label="Edit Scenarios"
|
sx={{ ...stickyHeaderStyle, top: topValue }}
|
||||||
icon={<Icon as={loading ? Spinner : BsGear} />}
|
>
|
||||||
/>
|
<HStack w="100%">
|
||||||
</MenuButton>
|
<Heading size="xs" fontWeight="bold" flex={1}>
|
||||||
<MenuList fontSize="md">
|
Scenarios ({numScenarios})
|
||||||
<MenuItem icon={<Icon as={BsPlus} boxSize={6} />} onClick={() => onAddScenario(false)}>
|
</Heading>
|
||||||
Add Scenario
|
{canModify && (
|
||||||
</MenuItem>
|
<Button
|
||||||
<MenuItem icon={<BsStars />} onClick={() => onAddScenario(true)}>
|
size="xs"
|
||||||
Autogenerate Scenario
|
variant="ghost"
|
||||||
</MenuItem>
|
color="gray.500"
|
||||||
<MenuItem icon={<BsPencil />} onClick={openDrawer}>
|
aria-label="Edit"
|
||||||
Edit Vars
|
leftIcon={<BsPencil />}
|
||||||
</MenuItem>
|
onClick={openDrawer}
|
||||||
</MenuList>
|
>
|
||||||
</Menu>
|
Edit Vars
|
||||||
)}
|
</Button>
|
||||||
</HStack>
|
)}
|
||||||
|
</HStack>
|
||||||
|
</GridItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,47 @@
|
|||||||
import { Box, Button, HStack, Spinner, Tooltip, useToast, Text } from "@chakra-ui/react";
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
HStack,
|
||||||
|
Spinner,
|
||||||
|
Tooltip,
|
||||||
|
useToast,
|
||||||
|
Text,
|
||||||
|
IconButton,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
import { useRef, useEffect, useState, useCallback } from "react";
|
import { useRef, useEffect, useState, useCallback } from "react";
|
||||||
import { useExperimentAccess, useHandledAsyncCallback, useModifierKeyLabel } from "~/utils/hooks";
|
import { useExperimentAccess, useHandledAsyncCallback, useModifierKeyLabel } from "~/utils/hooks";
|
||||||
import { type PromptVariant } from "./types";
|
import { type PromptVariant } from "./types";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { useAppStore } from "~/state/store";
|
import { useAppStore } from "~/state/store";
|
||||||
|
import { FiMaximize, FiMinimize } from "react-icons/fi";
|
||||||
|
import { editorBackground } from "~/state/sharedVariantEditor.slice";
|
||||||
|
|
||||||
export default function VariantEditor(props: { variant: PromptVariant }) {
|
export default function VariantEditor(props: { variant: PromptVariant }) {
|
||||||
const { canModify } = useExperimentAccess();
|
const { canModify } = useExperimentAccess();
|
||||||
const monaco = useAppStore.use.sharedVariantEditor.monaco();
|
const monaco = useAppStore.use.sharedVariantEditor.monaco();
|
||||||
const editorRef = useRef<ReturnType<NonNullable<typeof monaco>["editor"]["create"]> | null>(null);
|
const editorRef = useRef<ReturnType<NonNullable<typeof monaco>["editor"]["create"]> | null>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [editorId] = useState(() => `editor_${Math.random().toString(36).substring(7)}`);
|
const [editorId] = useState(() => `editor_${Math.random().toString(36).substring(7)}`);
|
||||||
const [isChanged, setIsChanged] = useState(false);
|
const [isChanged, setIsChanged] = useState(false);
|
||||||
|
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
|
||||||
|
const toggleFullscreen = useCallback(() => {
|
||||||
|
setIsFullscreen((prev) => !prev);
|
||||||
|
editorRef.current?.focus();
|
||||||
|
}, [setIsFullscreen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEsc = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape" && isFullscreen) {
|
||||||
|
toggleFullscreen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleEsc);
|
||||||
|
return () => window.removeEventListener("keydown", handleEsc);
|
||||||
|
}, [isFullscreen, toggleFullscreen]);
|
||||||
|
|
||||||
const lastSavedFn = props.variant.constructFn;
|
const lastSavedFn = props.variant.constructFn;
|
||||||
|
|
||||||
const modifierKey = useModifierKeyLabel();
|
const modifierKey = useModifierKeyLabel();
|
||||||
@@ -99,11 +129,23 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
|||||||
readOnly: !canModify,
|
readOnly: !canModify,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Workaround because otherwise the commands only work on whatever
|
||||||
|
// editor was loaded on the page last.
|
||||||
|
// https://github.com/microsoft/monaco-editor/issues/2947#issuecomment-1422265201
|
||||||
editorRef.current.onDidFocusEditorText(() => {
|
editorRef.current.onDidFocusEditorText(() => {
|
||||||
// Workaround because otherwise the command only works on whatever
|
editorRef.current?.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, onSave);
|
||||||
// editor was loaded on the page last.
|
|
||||||
// https://github.com/microsoft/monaco-editor/issues/2947#issuecomment-1422265201
|
editorRef.current?.addCommand(
|
||||||
editorRef.current?.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, onSave);
|
monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF,
|
||||||
|
toggleFullscreen,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Exit fullscreen with escape
|
||||||
|
editorRef.current?.addCommand(monaco.KeyCode.Escape, () => {
|
||||||
|
if (isFullscreen) {
|
||||||
|
toggleFullscreen();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
editorRef.current.onDidChangeModelContent(checkForChanges);
|
editorRef.current.onDidChangeModelContent(checkForChanges);
|
||||||
@@ -132,8 +174,40 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
|||||||
}, [canModify]);
|
}, [canModify]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box w="100%" pos="relative">
|
<Box
|
||||||
<div id={editorId} style={{ height: "400px", width: "100%" }}></div>
|
w="100%"
|
||||||
|
ref={containerRef}
|
||||||
|
sx={
|
||||||
|
isFullscreen
|
||||||
|
? {
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
}
|
||||||
|
: { h: "400px", w: "100%" }
|
||||||
|
}
|
||||||
|
bgColor={editorBackground}
|
||||||
|
zIndex={isFullscreen ? 1000 : "unset"}
|
||||||
|
pos="relative"
|
||||||
|
_hover={{ ".fullscreen-toggle": { opacity: 1 } }}
|
||||||
|
>
|
||||||
|
<Box id={editorId} w="100%" h="100%" />
|
||||||
|
<Tooltip label={`${modifierKey} + ⇧ + F`}>
|
||||||
|
<IconButton
|
||||||
|
className="fullscreen-toggle"
|
||||||
|
aria-label="Minimize"
|
||||||
|
icon={isFullscreen ? <FiMinimize /> : <FiMaximize />}
|
||||||
|
position="absolute"
|
||||||
|
top={2}
|
||||||
|
right={2}
|
||||||
|
onClick={toggleFullscreen}
|
||||||
|
opacity={0}
|
||||||
|
transition="opacity 0.2s"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{isChanged && (
|
{isChanged && (
|
||||||
<HStack pos="absolute" bottom={2} right={2}>
|
<HStack pos="absolute" bottom={2} right={2}>
|
||||||
<Button
|
<Button
|
||||||
@@ -146,7 +220,7 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
|||||||
>
|
>
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
<Tooltip label={`${modifierKey} + Enter`}>
|
<Tooltip label={`${modifierKey} + S`}>
|
||||||
<Button size="sm" onClick={onSave} colorScheme="blue" w={16} disabled={saveInProgress}>
|
<Button size="sm" onClick={onSave} colorScheme="blue" w={16} disabled={saveInProgress}>
|
||||||
{saveInProgress ? <Spinner boxSize={4} /> : <Text>Save</Text>}
|
{saveInProgress ? <Spinner boxSize={4} /> : <Text>Save</Text>}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Grid, GridItem, type GridItemProps } from "@chakra-ui/react";
|
import { Grid, GridItem } from "@chakra-ui/react";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import AddVariantButton from "./AddVariantButton";
|
import NewScenarioButton from "./NewScenarioButton";
|
||||||
|
import NewVariantButton from "./NewVariantButton";
|
||||||
import ScenarioRow from "./ScenarioRow";
|
import ScenarioRow from "./ScenarioRow";
|
||||||
import VariantEditor from "./VariantEditor";
|
import VariantEditor from "./VariantEditor";
|
||||||
import VariantHeader from "../VariantHeader/VariantHeader";
|
import VariantHeader from "../VariantHeader/VariantHeader";
|
||||||
import VariantStats from "./VariantStats";
|
import VariantStats from "./VariantStats";
|
||||||
import { ScenariosHeader } from "./ScenariosHeader";
|
import { ScenariosHeader } from "./ScenariosHeader";
|
||||||
import { borders } from "./styles";
|
import { stickyHeaderStyle } from "./styles";
|
||||||
|
|
||||||
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(
|
||||||
@@ -21,76 +22,61 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
|||||||
|
|
||||||
if (!variants.data || !scenarios.data) return null;
|
if (!variants.data || !scenarios.data) return null;
|
||||||
|
|
||||||
const allCols = variants.data.length + 2;
|
const allCols = variants.data.length + 1;
|
||||||
const variantHeaderRows = 3;
|
const headerRows = 3;
|
||||||
const scenarioHeaderRows = 1;
|
|
||||||
const allRows = variantHeaderRows + scenarioHeaderRows + scenarios.data.length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
pt={4}
|
p={4}
|
||||||
pb={24}
|
pb={24}
|
||||||
pl={4}
|
|
||||||
display="grid"
|
display="grid"
|
||||||
gridTemplateColumns={`250px repeat(${variants.data.length}, minmax(300px, 1fr)) auto`}
|
gridTemplateColumns={`250px repeat(${variants.data.length}, minmax(300px, 1fr)) auto`}
|
||||||
sx={{
|
sx={{
|
||||||
"> *": {
|
"> *": {
|
||||||
borderColor: "gray.300",
|
borderColor: "gray.300",
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
>
|
>
|
||||||
<GridItem rowSpan={variantHeaderRows}>
|
<ScenariosHeader headerRows={headerRows} numScenarios={scenarios.data.length} />
|
||||||
<AddVariantButton />
|
|
||||||
</GridItem>
|
|
||||||
|
|
||||||
{variants.data.map((variant, i) => {
|
|
||||||
const sharedProps: GridItemProps = {
|
|
||||||
...borders,
|
|
||||||
colStart: i + 2,
|
|
||||||
borderLeftWidth: i === 0 ? 1 : 0,
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<VariantHeader
|
|
||||||
key={variant.uiId}
|
|
||||||
variant={variant}
|
|
||||||
canHide={variants.data.length > 1}
|
|
||||||
rowStart={1}
|
|
||||||
{...sharedProps}
|
|
||||||
/>
|
|
||||||
<GridItem rowStart={2} {...sharedProps}>
|
|
||||||
<VariantEditor variant={variant} />
|
|
||||||
</GridItem>
|
|
||||||
<GridItem rowStart={3} {...sharedProps}>
|
|
||||||
<VariantStats variant={variant} />
|
|
||||||
</GridItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
|
{variants.data.map((variant) => (
|
||||||
|
<VariantHeader key={variant.uiId} variant={variant} canHide={variants.data.length > 1} />
|
||||||
|
))}
|
||||||
<GridItem
|
<GridItem
|
||||||
colSpan={allCols - 1}
|
rowSpan={scenarios.data.length + headerRows}
|
||||||
rowStart={variantHeaderRows + 1}
|
padding={0}
|
||||||
colStart={1}
|
// Have to use `style` instead of emotion style props to work around css specificity issues conflicting with the "> *" selector on Grid
|
||||||
{...borders}
|
style={{ borderRightWidth: 0, borderBottomWidth: 0 }}
|
||||||
borderRightWidth={0}
|
h={8}
|
||||||
|
sx={stickyHeaderStyle}
|
||||||
>
|
>
|
||||||
<ScenariosHeader numScenarios={scenarios.data.length} />
|
<NewVariantButton />
|
||||||
</GridItem>
|
</GridItem>
|
||||||
|
|
||||||
{scenarios.data.map((scenario, i) => (
|
{variants.data.map((variant) => (
|
||||||
|
<GridItem key={variant.uiId}>
|
||||||
|
<VariantEditor variant={variant} />
|
||||||
|
</GridItem>
|
||||||
|
))}
|
||||||
|
{variants.data.map((variant) => (
|
||||||
|
<GridItem key={variant.uiId}>
|
||||||
|
<VariantStats variant={variant} />
|
||||||
|
</GridItem>
|
||||||
|
))}
|
||||||
|
{scenarios.data.map((scenario) => (
|
||||||
<ScenarioRow
|
<ScenarioRow
|
||||||
rowStart={i + variantHeaderRows + scenarioHeaderRows + 2}
|
|
||||||
key={scenario.uiId}
|
key={scenario.uiId}
|
||||||
scenario={scenario}
|
scenario={scenario}
|
||||||
variants={variants.data}
|
variants={variants.data}
|
||||||
canHide={scenarios.data.length > 1}
|
canHide={scenarios.data.length > 1}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
<GridItem borderBottomWidth={0} borderRightWidth={0} w="100%" colSpan={allCols} padding={0}>
|
||||||
{/* Add some extra padding on the right, because when the table is too wide to fit in the viewport `pr` on the Grid isn't respected. */}
|
<NewScenarioButton />
|
||||||
<GridItem rowStart={1} colStart={allCols} rowSpan={allRows} w={4} borderBottomWidth={0} />
|
</GridItem>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type GridItemProps, type SystemStyleObject } from "@chakra-ui/react";
|
import { type SystemStyleObject } from "@chakra-ui/react";
|
||||||
|
|
||||||
export const stickyHeaderStyle: SystemStyleObject = {
|
export const stickyHeaderStyle: SystemStyleObject = {
|
||||||
position: "sticky",
|
position: "sticky",
|
||||||
@@ -6,8 +6,3 @@ export const stickyHeaderStyle: SystemStyleObject = {
|
|||||||
backgroundColor: "#fff",
|
backgroundColor: "#fff",
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const borders: GridItemProps = {
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -3,34 +3,28 @@ import { type PromptVariant } from "../OutputsTable/types";
|
|||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { RiDraggable } from "react-icons/ri";
|
import { RiDraggable } from "react-icons/ri";
|
||||||
import { useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
import { useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
import { HStack, Icon, Text, GridItem, type GridItemProps } from "@chakra-ui/react"; // Changed here
|
import { HStack, Icon, Text, GridItem } from "@chakra-ui/react"; // Changed here
|
||||||
import { cellPadding, headerMinHeight } from "../constants";
|
import { cellPadding, headerMinHeight } from "../constants";
|
||||||
import AutoResizeTextArea from "../AutoResizeTextArea";
|
import AutoResizeTextArea from "../AutoResizeTextArea";
|
||||||
import { stickyHeaderStyle } from "../OutputsTable/styles";
|
import { stickyHeaderStyle } from "../OutputsTable/styles";
|
||||||
import VariantHeaderMenuButton from "./VariantHeaderMenuButton";
|
import VariantHeaderMenuButton from "./VariantHeaderMenuButton";
|
||||||
|
|
||||||
export default function VariantHeader(
|
export default function VariantHeader(props: { variant: PromptVariant; canHide: boolean }) {
|
||||||
allProps: {
|
|
||||||
variant: PromptVariant;
|
|
||||||
canHide: boolean;
|
|
||||||
} & GridItemProps,
|
|
||||||
) {
|
|
||||||
const { variant, canHide, ...gridItemProps } = allProps;
|
|
||||||
const { canModify } = useExperimentAccess();
|
const { canModify } = useExperimentAccess();
|
||||||
const utils = api.useContext();
|
const utils = api.useContext();
|
||||||
const [isDragTarget, setIsDragTarget] = useState(false);
|
const [isDragTarget, setIsDragTarget] = useState(false);
|
||||||
const [isInputHovered, setIsInputHovered] = useState(false);
|
const [isInputHovered, setIsInputHovered] = useState(false);
|
||||||
const [label, setLabel] = useState(variant.label);
|
const [label, setLabel] = useState(props.variant.label);
|
||||||
|
|
||||||
const updateMutation = api.promptVariants.update.useMutation();
|
const updateMutation = api.promptVariants.update.useMutation();
|
||||||
const [onSaveLabel] = useHandledAsyncCallback(async () => {
|
const [onSaveLabel] = useHandledAsyncCallback(async () => {
|
||||||
if (label && label !== variant.label) {
|
if (label && label !== props.variant.label) {
|
||||||
await updateMutation.mutateAsync({
|
await updateMutation.mutateAsync({
|
||||||
id: variant.id,
|
id: props.variant.id,
|
||||||
updates: { label: label },
|
updates: { label: label },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [updateMutation, variant.id, variant.label, label]);
|
}, [updateMutation, props.variant.id, props.variant.label, label]);
|
||||||
|
|
||||||
const reorderMutation = api.promptVariants.reorder.useMutation();
|
const reorderMutation = api.promptVariants.reorder.useMutation();
|
||||||
const [onReorder] = useHandledAsyncCallback(
|
const [onReorder] = useHandledAsyncCallback(
|
||||||
@@ -38,7 +32,7 @@ export default function VariantHeader(
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsDragTarget(false);
|
setIsDragTarget(false);
|
||||||
const draggedId = e.dataTransfer.getData("text/plain");
|
const draggedId = e.dataTransfer.getData("text/plain");
|
||||||
const droppedId = variant.id;
|
const droppedId = props.variant.id;
|
||||||
if (!draggedId || !droppedId || draggedId === droppedId) return;
|
if (!draggedId || !droppedId || draggedId === droppedId) return;
|
||||||
await reorderMutation.mutateAsync({
|
await reorderMutation.mutateAsync({
|
||||||
draggedId,
|
draggedId,
|
||||||
@@ -46,16 +40,16 @@ export default function VariantHeader(
|
|||||||
});
|
});
|
||||||
await utils.promptVariants.list.invalidate();
|
await utils.promptVariants.list.invalidate();
|
||||||
},
|
},
|
||||||
[reorderMutation, variant.id],
|
[reorderMutation, props.variant.id],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
|
||||||
if (!canModify) {
|
if (!canModify) {
|
||||||
return (
|
return (
|
||||||
<GridItem padding={0} sx={stickyHeaderStyle} borderTopWidth={1} {...gridItemProps}>
|
<GridItem padding={0} sx={stickyHeaderStyle} borderTopWidth={1}>
|
||||||
<Text fontSize={16} fontWeight="bold" px={cellPadding.x} py={cellPadding.y}>
|
<Text fontSize={16} fontWeight="bold" px={cellPadding.x} py={cellPadding.y}>
|
||||||
{variant.label}
|
{props.variant.label}
|
||||||
</Text>
|
</Text>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
);
|
);
|
||||||
@@ -70,7 +64,6 @@ export default function VariantHeader(
|
|||||||
zIndex: menuOpen ? "dropdown" : stickyHeaderStyle.zIndex,
|
zIndex: menuOpen ? "dropdown" : stickyHeaderStyle.zIndex,
|
||||||
}}
|
}}
|
||||||
borderTopWidth={1}
|
borderTopWidth={1}
|
||||||
{...gridItemProps}
|
|
||||||
>
|
>
|
||||||
<HStack
|
<HStack
|
||||||
spacing={4}
|
spacing={4}
|
||||||
@@ -78,7 +71,7 @@ export default function VariantHeader(
|
|||||||
minH={headerMinHeight}
|
minH={headerMinHeight}
|
||||||
draggable={!isInputHovered}
|
draggable={!isInputHovered}
|
||||||
onDragStart={(e) => {
|
onDragStart={(e) => {
|
||||||
e.dataTransfer.setData("text/plain", variant.id);
|
e.dataTransfer.setData("text/plain", props.variant.id);
|
||||||
e.currentTarget.style.opacity = "0.4";
|
e.currentTarget.style.opacity = "0.4";
|
||||||
}}
|
}}
|
||||||
onDragEnd={(e) => {
|
onDragEnd={(e) => {
|
||||||
@@ -119,8 +112,8 @@ export default function VariantHeader(
|
|||||||
onMouseLeave={() => setIsInputHovered(false)}
|
onMouseLeave={() => setIsInputHovered(false)}
|
||||||
/>
|
/>
|
||||||
<VariantHeaderMenuButton
|
<VariantHeaderMenuButton
|
||||||
variant={variant}
|
variant={props.variant}
|
||||||
canHide={canHide}
|
canHide={props.canHide}
|
||||||
menuOpen={menuOpen}
|
menuOpen={menuOpen}
|
||||||
setMenuOpen={setMenuOpen}
|
setMenuOpen={setMenuOpen}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -34,21 +34,22 @@ export const scenariosRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await requireCanModifyExperiment(input.experimentId, ctx);
|
await requireCanModifyExperiment(input.experimentId, ctx);
|
||||||
|
|
||||||
await prisma.testScenario.updateMany({
|
const maxSortIndex =
|
||||||
where: {
|
(
|
||||||
experimentId: input.experimentId,
|
await prisma.testScenario.aggregate({
|
||||||
},
|
where: {
|
||||||
data: {
|
experimentId: input.experimentId,
|
||||||
sortIndex: {
|
},
|
||||||
increment: 1,
|
_max: {
|
||||||
},
|
sortIndex: true,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
)._max.sortIndex ?? 0;
|
||||||
|
|
||||||
const createNewScenarioAction = prisma.testScenario.create({
|
const createNewScenarioAction = prisma.testScenario.create({
|
||||||
data: {
|
data: {
|
||||||
experimentId: input.experimentId,
|
experimentId: input.experimentId,
|
||||||
sortIndex: 0,
|
sortIndex: maxSortIndex + 1,
|
||||||
variableValues: input.autogenerate
|
variableValues: input.autogenerate
|
||||||
? await autogenerateScenarioValues(input.experimentId)
|
? await autogenerateScenarioValues(input.experimentId)
|
||||||
: {},
|
: {},
|
||||||
|
|||||||
Reference in New Issue
Block a user