Skip to content

Commit

Permalink
feat(prompts): Implement SavePromptForm for creating new prompts (#5751)
Browse files Browse the repository at this point in the history
* feat(prompts): Implement SavePromptForm for creating new prompts

* Rename button to 'save'
  • Loading branch information
cephalization authored and mikeldking committed Dec 26, 2024
1 parent 38381d4 commit 4f4577e
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 51 deletions.
2 changes: 1 addition & 1 deletion app/src/pages/playground/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ const playgroundInputOutputPanelContentCSS = css`
* This width accomodates the model config button min-width, as well as chat message accordion
* header contents such as the chat message mode radio group for AI messages
*/
const PLAYGROUND_PROMPT_PANEL_MIN_WIDTH = 570;
const PLAYGROUND_PROMPT_PANEL_MIN_WIDTH = 632;

function PlaygroundContent() {
const instances = usePlaygroundContext((state) => state.instances);
Expand Down
159 changes: 111 additions & 48 deletions app/src/pages/playground/PlaygroundTemplate.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React, { Suspense } from "react";
import React, { Suspense, useCallback, useState } from "react";

import {
Card,
Content,
Dialog,
DialogContainer,
Flex,
Tooltip,
TooltipTrigger,
Expand All @@ -17,11 +19,13 @@ import { ModelConfigButton } from "./ModelConfigButton";
import { ModelSupportedParamsFetcher } from "./ModelSupportedParamsFetcher";
import { PlaygroundChatTemplate } from "./PlaygroundChatTemplate";
import { PromptComboBox } from "./PromptComboBox";
import { SavePromptForm, SavePromptSubmitHandler } from "./SavePromptForm";
import { PlaygroundInstanceProps } from "./types";

interface PlaygroundTemplateProps extends PlaygroundInstanceProps {}

export function PlaygroundTemplate(props: PlaygroundTemplateProps) {
const [dialog, setDialog] = useState<React.ReactNode>(null);
const instanceId = props.playgroundInstanceId;
const instances = usePlaygroundContext((state) => state.instances);
const instance = instances.find((instance) => instance.id === instanceId);
Expand All @@ -42,55 +46,66 @@ export function PlaygroundTemplate(props: PlaygroundTemplateProps) {
const { template } = instance;

return (
<Card
title={
<Flex
direction="row"
gap="size-100"
alignItems="center"
marginEnd="size-100"
>
<AlphabeticIndexIcon index={index} />
<PromptComboBox
promptId={promptId}
onChange={(nextPromptId) => {
updateInstancePrompt({
instanceId,
patch: nextPromptId ? { id: nextPromptId } : null,
});
}}
/>
</Flex>
}
collapsible
variant="compact"
bodyStyle={{ padding: 0 }}
extra={
<Flex direction="row" gap="size-100">
<Suspense
fallback={
<div>
<Loading size="S" />
</div>
}
<>
<Card
title={
<Flex
direction="row"
gap="size-100"
alignItems="center"
marginEnd="size-100"
>
{/* As long as this component mounts, it will sync the supported
invocation parameters for the model to the instance in the store */}
<ModelSupportedParamsFetcher instanceId={instanceId} />
<AlphabeticIndexIcon index={index} />
<PromptComboBox
promptId={promptId}
onChange={(nextPromptId) => {
updateInstancePrompt({
instanceId,
patch: nextPromptId ? { id: nextPromptId } : null,
});
}}
/>
</Flex>
}
collapsible
variant="compact"
bodyStyle={{ padding: 0 }}
extra={
<Flex direction="row" gap="size-100">
<Suspense
fallback={
<div>
<Loading size="S" />
</div>
}
>
{/* As long as this component mounts, it will sync the supported
invocation parameters for the model to the instance in the store */}
<ModelSupportedParamsFetcher instanceId={instanceId} />
</Suspense>
<ModelConfigButton {...props} />
<SaveButton instanceId={instanceId} setDialog={setDialog} />
{instances.length > 1 ? <DeleteButton {...props} /> : null}
</Flex>
}
>
{template.__type === "chat" ? (
<Suspense>
<PlaygroundChatTemplate {...props} />
</Suspense>
<ModelConfigButton {...props} />
{instances.length > 1 ? <DeleteButton {...props} /> : null}
</Flex>
}
>
{template.__type === "chat" ? (
<Suspense>
<PlaygroundChatTemplate {...props} />
</Suspense>
) : (
"Completion Template"
)}
</Card>
) : (
"Completion Template"
)}
</Card>
<DialogContainer
isDismissable
onDismiss={() => {
setDialog(null);
}}
>
{dialog}
</DialogContainer>
</>
);
}

Expand All @@ -101,6 +116,7 @@ function DeleteButton(props: PlaygroundInstanceProps) {
<TriggerWrap>
<Button
size="S"
aria-label="Delete this instance of the playground"
icon={<Icon svg={<Icons.TrashOutline />} />}
onPress={() => {
deleteInstance(props.playgroundInstanceId);
Expand All @@ -113,3 +129,50 @@ function DeleteButton(props: PlaygroundInstanceProps) {
</TooltipTrigger>
);
}

type SaveButtonProps = {
instanceId: number;
setDialog: (dialog: React.ReactNode) => void;
};

function SaveButton({ instanceId, setDialog }: SaveButtonProps) {
const instance = usePlaygroundContext((state) =>
state.instances.find((instance) => instance.id === instanceId)
);
if (!instance) {
throw new Error(`Instance ${instanceId} not found`);
}
const prompt = instance.prompt;
const onSubmit: SavePromptSubmitHandler = useCallback(
(params) => {
// eslint-disable-next-line no-console
console.log("saving prompt", instanceId, params);
},
[instanceId]
);
const onSave = () => {
setDialog(
<Dialog title={prompt?.id ? "Update Prompt" : "Save Prompt"}>
<SavePromptForm onSubmit={onSubmit} />
</Dialog>
);
};
return (
<>
<TooltipTrigger delay={100}>
<TriggerWrap>
<Button
// TODO(apowell): Make variant "primary" when instance is "dirty", aka different from selected prompt
size="S"
onPress={onSave}
>
Save
</Button>
</TriggerWrap>
<Tooltip>
<Content>Save this prompt</Content>
</Tooltip>
</TooltipTrigger>
</>
);
}
105 changes: 105 additions & 0 deletions app/src/pages/playground/SavePromptForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React from "react";
import { Controller, useForm } from "react-hook-form";

import {
Button,
Flex,
Form,
TextArea,
TextField,
View,
} from "@arizeai/components";

export type SavePromptSubmitHandler = (params: SavePromptFormParams) => void;

export type SavePromptFormParams = {
name: string;
description?: string;
};

export function SavePromptForm({
onSubmit,
isSubmitting = false,
submitButtonText = "Save",
}: {
onSubmit: SavePromptSubmitHandler;
isSubmitting?: boolean;
submitButtonText?: string;
}) {
const {
control,
handleSubmit,
formState: { isDirty },
} = useForm<SavePromptFormParams>({
defaultValues: {
name: "Prompt " + new Date().toISOString(),
description: "",
},
});

return (
<Form>
<View padding="size-200">
<Controller
name="name"
control={control}
rules={{
required: "Prompt name is required",
}}
render={({
field: { onChange, onBlur, value },
fieldState: { invalid, error },
}) => (
<TextField
label="Prompt Name"
description="The name of your saved prompt"
errorMessage={error?.message}
validationState={invalid ? "invalid" : "valid"}
onChange={onChange}
onBlur={onBlur}
value={value}
/>
)}
/>
<Controller
name="description"
control={control}
render={({
field: { onChange, onBlur, value },
fieldState: { invalid, error },
}) => (
<TextArea
label="Description"
description="A description of your prompt (optional)"
isRequired={false}
height={100}
errorMessage={error?.message}
validationState={invalid ? "invalid" : "valid"}
onChange={onChange}
onBlur={onBlur}
value={value}
/>
)}
/>
</View>
<View
paddingEnd="size-200"
paddingTop="size-100"
paddingBottom="size-100"
borderTopColor="light"
borderTopWidth="thin"
>
<Flex direction="row" justifyContent="end">
<Button
variant={isDirty ? "primary" : "default"}
size="compact"
loading={isSubmitting}
onClick={handleSubmit(onSubmit)}
>
{submitButtonText}
</Button>
</Flex>
</View>
</Form>
);
}
8 changes: 6 additions & 2 deletions app/src/pages/prompts/PromptsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { useLoaderData } from "react-router";
import { useLoaderData, useNavigate } from "react-router";

import { Button, Flex, Heading, Icon, Icons, View } from "@arizeai/components";

Expand All @@ -8,6 +8,7 @@ import { PromptsTable } from "./PromptsTable";

export function PromptsPage() {
const loaderData = useLoaderData() as promptsLoaderQuery$data;
const navigate = useNavigate();
return (
<Flex direction="column" height="100%">
<View
Expand All @@ -22,8 +23,11 @@ export function PromptsPage() {
variant="default"
size="compact"
icon={<Icon svg={<Icons.PlusOutline />} />}
onClick={() => {
navigate("/playground");
}}
>
Prompt Template
Create Prompt Template
</Button>
</Flex>
</View>
Expand Down

0 comments on commit 4f4577e

Please sign in to comment.