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.
158 lines
5.0 KiB
TypeScript
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>
|
|
);
|
|
}
|