EditPromptButton.tsx•6.62 kB
import { useCallback, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { graphql, useFragment, useMutation } from "react-relay";
import {
Button,
Dialog,
DialogCloseButton,
DialogHeader,
DialogTitle,
DialogTitleExtra,
DialogTrigger,
Flex,
Form,
Icon,
Icons,
Label,
Modal,
Text,
TextArea,
TextField,
View,
} from "@phoenix/components";
import { CodeEditorFieldWrapper, JSONEditor } from "@phoenix/components/code";
import { useNotifyError, useNotifySuccess } from "@phoenix/contexts";
import { getErrorMessagesFromRelayMutationError } from "@phoenix/utils/errorUtils";
import { isJSONObjectString } from "@phoenix/utils/jsonUtils";
import { EditPromptButton_data$key } from "./__generated__/EditPromptButton_data.graphql";
import { EditPromptButtonPatchPromptMutation } from "./__generated__/EditPromptButtonPatchPromptMutation.graphql";
type EditPromptFormParams = {
description: string;
metadata: string;
};
export function EditPromptButton(props: { prompt: EditPromptButton_data$key }) {
const [isOpen, setIsOpen] = useState(false);
const notifySuccess = useNotifySuccess();
const notifyError = useNotifyError();
const data = useFragment(
graphql`
fragment EditPromptButton_data on Prompt {
id
description
metadata
}
`,
props.prompt
);
const [mutatePrompt, isMutating] =
useMutation<EditPromptButtonPatchPromptMutation>(graphql`
mutation EditPromptButtonPatchPromptMutation($input: PatchPromptInput!) {
patchPrompt(input: $input) {
...PromptIndexPage__main
}
}
`);
const { control, handleSubmit, reset } = useForm<EditPromptFormParams>({
defaultValues: {
description: data.description ?? "",
metadata: data.metadata ? JSON.stringify(data.metadata, null, 2) : "{}",
},
});
const handleOpenChange = useCallback(
(open: boolean) => {
setIsOpen(open);
if (open) {
reset({
description: data.description ?? "",
metadata: data.metadata
? JSON.stringify(data.metadata, null, 2)
: "{}",
});
}
},
[data, reset]
);
const onSubmit = useCallback(
(promptPatch: EditPromptFormParams) => {
// Parse metadata, or set to null to clear if empty
let metadata: unknown = null;
if (promptPatch.metadata && promptPatch.metadata.trim() !== "") {
try {
metadata = JSON.parse(promptPatch.metadata);
} catch (_error) {
notifyError({
title: "Invalid metadata",
message: "Failed to parse metadata as JSON",
});
return;
}
}
mutatePrompt({
variables: {
input: {
promptId: data.id,
description: promptPatch.description,
metadata,
},
},
onCompleted: () => {
setIsOpen(false);
notifySuccess({
title: "Prompt updated",
message: "The prompt has been updated successfully",
});
},
onError: (error) => {
notifyError({
title: "Error updating prompt",
message: getErrorMessagesFromRelayMutationError(error)?.[0],
});
},
});
},
[data.id, mutatePrompt, notifyError, notifySuccess]
);
return (
<DialogTrigger isOpen={isOpen} onOpenChange={handleOpenChange}>
<Button
size="S"
leadingVisual={<Icon svg={<Icons.SettingsOutline />} />}
variant="quiet"
aria-label="configure prompt"
/>
<Modal size="M" isDismissable>
<Dialog>
<DialogHeader>
<DialogTitle>Edit Prompt Details</DialogTitle>
<DialogTitleExtra>
<DialogCloseButton />
</DialogTitleExtra>
</DialogHeader>
<Form>
<View padding="size-200">
<Flex direction="column" gap="size-100">
<Controller
name="description"
control={control}
render={({ field }) => (
<TextField value={field.value} onChange={field.onChange}>
<Label>Prompt Description</Label>
<TextArea placeholder="Enter a description for the prompt" />
<Text slot="description">
A description of the prompt
</Text>
</TextField>
)}
/>
<Controller
name="metadata"
control={control}
rules={{
validate: (value) => {
// Allow empty values (will be treated as null)
if (!value || value.trim() === "") {
return true;
}
if (!isJSONObjectString(value)) {
return "metadata must be a valid JSON object";
}
return true;
},
}}
render={({
field: { onChange, onBlur, value },
fieldState: { error },
}) => (
<CodeEditorFieldWrapper
label={"Metadata"}
errorMessage={error?.message}
description="A JSON object containing metadata for the prompt"
>
<JSONEditor
value={value}
onChange={onChange}
onBlur={onBlur}
/>
</CodeEditorFieldWrapper>
)}
/>
</Flex>
</View>
<View
paddingX="size-200"
paddingY="size-100"
borderTopColor="light"
borderTopWidth="thin"
>
<Flex direction="row" gap="size-100" justifyContent="end">
<Button slot="close" size="S">
Cancel
</Button>
<Button
variant="primary"
size="S"
isDisabled={isMutating}
onPress={() => handleSubmit(onSubmit)()}
>
{isMutating ? "Saving..." : "Save"}
</Button>
</Flex>
</View>
</Form>
</Dialog>
</Modal>
</DialogTrigger>
);
}