Files
OpenPipe-llm/src/components/OutputsTable/VariantEditor.tsx
Kyle Corbitt 8db8aeacd3 Replace function chrome with comment
Use a block comment to explain the expected prompt formatting instead of function chrome. The advantage here is that once a user builds a mental model of how OpenPipe works they can just delete the comment, instead of the function chrome sitting around and taking up space in the UI forever.
2023-07-17 10:30:22 -07:00

158 lines
5.0 KiB
TypeScript

import { Box, Button, HStack, Tooltip, useToast } from "@chakra-ui/react";
import { useRef, useEffect, useState, useCallback } from "react";
import { useHandledAsyncCallback, useModifierKeyLabel } from "~/utils/hooks";
import { type PromptVariant } from "./types";
import { api } from "~/utils/api";
import { useAppStore } from "~/state/store";
export default function VariantEditor(props: { variant: PromptVariant }) {
const monaco = useAppStore.use.sharedVariantEditor.monaco();
const editorRef = useRef<ReturnType<NonNullable<typeof monaco>["editor"]["create"]> | null>(null);
const [editorId] = useState(() => `editor_${Math.random().toString(36).substring(7)}`);
const [isChanged, setIsChanged] = useState(false);
const lastSavedFn = props.variant.constructFn;
const modifierKey = useModifierKeyLabel();
const checkForChanges = useCallback(() => {
if (!editorRef.current) return;
const currentFn = editorRef.current.getValue();
setIsChanged(currentFn.length > 0 && currentFn !== lastSavedFn);
}, [lastSavedFn]);
useEffect(checkForChanges, [checkForChanges, lastSavedFn]);
const replaceVariant = api.promptVariants.replaceVariant.useMutation();
const utils = api.useContext();
const toast = useToast();
const [onSave] = useHandledAsyncCallback(async () => {
if (!editorRef.current) return;
await editorRef.current.getAction("editor.action.formatDocument")?.run();
const currentFn = editorRef.current.getValue();
if (!currentFn) return;
// Check if the editor has any typescript errors
const model = editorRef.current.getModel();
if (!model) return;
const markers = monaco?.editor.getModelMarkers({ resource: model.uri });
const hasErrors = markers?.some((m) => m.severity === monaco?.MarkerSeverity.Error);
if (hasErrors) {
toast({
title: "Invalid TypeScript",
description: "Please fix the TypeScript errors before saving.",
status: "error",
});
return;
}
// Make sure the user defined the prompt with the string "prompt\w*=" somewhere
const promptRegex = /prompt\s*=/;
if (!promptRegex.test(currentFn)) {
console.log("no prompt");
console.log(currentFn);
toast({
title: "Missing prompt",
description: "Please define the prompt (eg. `prompt = { ...`).",
status: "error",
});
return;
}
const resp = await replaceVariant.mutateAsync({
id: props.variant.id,
constructFn: currentFn,
});
if (resp.status === "error") {
return toast({
title: "Error saving variant",
description: resp.message,
status: "error",
});
}
setIsChanged(false);
await utils.promptVariants.list.invalidate();
}, [checkForChanges]);
useEffect(() => {
if (monaco) {
const container = document.getElementById(editorId) as HTMLElement;
editorRef.current = monaco.editor.create(container, {
value: lastSavedFn,
language: "typescript",
theme: "customTheme",
lineNumbers: "off",
minimap: { enabled: false },
wrappingIndent: "indent",
wrappingStrategy: "advanced",
wordWrap: "on",
folding: false,
scrollbar: {
alwaysConsumeMouseWheel: false,
verticalScrollbarSize: 0,
},
wordWrapBreakAfterCharacters: "",
wordWrapBreakBeforeCharacters: "",
quickSuggestions: true,
});
editorRef.current.onDidFocusEditorText(() => {
// Workaround because otherwise the command only works on whatever
// editor was loaded on the page last.
// https://github.com/microsoft/monaco-editor/issues/2947#issuecomment-1422265201
editorRef.current?.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, onSave);
});
editorRef.current.onDidChangeModelContent(checkForChanges);
const resizeObserver = new ResizeObserver(() => {
editorRef.current?.layout();
});
resizeObserver.observe(container);
return () => {
resizeObserver.disconnect();
editorRef.current?.dispose();
};
}
// We intentionally skip the onSave and props.savedConfig dependencies here because
// we don't want to re-render the editor from scratch
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [monaco, editorId]);
return (
<Box w="100%" pos="relative">
<div id={editorId} style={{ height: "400px", width: "100%" }}></div>
{isChanged && (
<HStack pos="absolute" bottom={2} right={2}>
<Button
colorScheme="gray"
size="sm"
onClick={() => {
editorRef.current?.setValue(lastSavedFn);
checkForChanges();
}}
>
Reset
</Button>
<Tooltip label={`${modifierKey} + Enter`}>
<Button size="sm" onClick={onSave} colorScheme="blue">
Save
</Button>
</Tooltip>
</HStack>
)}
</Box>
);
}