Skip to content

Commit

Permalink
[editor] Config Name & Description UI
Browse files Browse the repository at this point in the history
[editor] Config Name & Description UI

# [editor] Config Name & Description UI

Add the UI for changing config name / description, similar to workbook title UI:


https://github.com/lastmile-ai/aiconfig/assets/5060851/f0f49638-d0c5-4d88-8edd-c0bbb0294f7c



If config name is not set, keep the name / description in Edit state until the name is set
  • Loading branch information
Ryan Holinshead committed Jan 2, 2024
1 parent 7cf4a63 commit d114595
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 16 deletions.
16 changes: 16 additions & 0 deletions python/src/aiconfig/editor/client/src/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,27 @@ export default function Editor() {
[]
);

const setConfigName = useCallback(async (name: string) => {
return await ufetch.post(ROUTE_TABLE.SET_NAME, {
name,
});
}, []);

const setConfigDescription = useCallback(async (description: string) => {
return await ufetch.post(ROUTE_TABLE.SET_DESCRIPTION, {
description,
});
}, []);

const callbacks: AIConfigCallbacks = useMemo(
() => ({
addPrompt,
deletePrompt,
getModels,
runPrompt,
save,
setConfigDescription,
setConfigName,
updateModel,
updatePrompt,
}),
Expand All @@ -98,6 +112,8 @@ export default function Editor() {
getModels,
runPrompt,
save,
setConfigDescription,
setConfigName,
updateModel,
updatePrompt,
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import {
createStyles,
Stack,
Text,
Textarea,
TextInput,
Title,
} from "@mantine/core";
import { useClickOutside } from "@mantine/hooks";
import { memo, useCallback, useRef, useState } from "react";

type Props = {
name?: string;
description?: string | null;
setDescription: (description: string) => void;
setName: (name: string) => void;
};

const useStyles = createStyles((theme) => ({
// Match name input style to corresponding Title element styles
// See https://github.com/mantinedev/mantine/blob/master/src/mantine-core/src/Title/Title.styles.ts
nameInput: {
...theme.fn.fontStyles(),
fontFamily: theme.headings.fontFamily,
fontWeight: theme.headings.fontWeight as number,
fontSize: theme.headings.sizes.h1.fontSize,
lineHeight: theme.headings.sizes.h1.lineHeight,
width: "-webkit-fill-available",
letterSpacing: "-1px",
height: "44px",
},
hoverContainer: {
"&:hover": {
backgroundColor:
theme.colorScheme === "dark"
? "rgba(255, 255, 255, 0.1)"
: theme.colors.gray[1],
},
borderRadius: theme.radius.sm,
width: "-webkit-fill-available",
},
}));

export default memo(function ConfigNameDescription({
name,
description,
setDescription,
setName,
}: Props) {
const { classes } = useStyles();

const [isEditing, setIsEditing] = useState(!name);
const [initialFocus, setInitialFocus] = useState<
"name" | "description" | null
>("name");

const nameDisplayRef = useRef<HTMLHeadingElement | null>(null);
const descriptionDisplayRef = useRef<HTMLDivElement | null>(null);

const inputSectionRef = useClickOutside(() => {
if (name) {
setIsEditing(false);
}
});

const onHandleEnter = useCallback(
(event: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
if (event.key === "Enter") {
event.stopPropagation();
setIsEditing(false);
}
},
[]
);

const onClickEdit = useCallback(
(event: React.MouseEvent<HTMLHeadingElement | HTMLDivElement>) => {
setIsEditing(true);
if (event.target === nameDisplayRef.current) {
setInitialFocus("name");
} else if (event.target === descriptionDisplayRef.current) {
setInitialFocus("description");
}
},
[]
);

return (
<Stack
ref={isEditing ? inputSectionRef : undefined}
spacing="xs"
w="100%"
ml="1em"
mr="0.5em"
>
{isEditing ? (
<>
<TextInput
classNames={{ input: classes.nameInput }}
placeholder={"Config name"}
value={name}
onKeyDown={onHandleEnter}
autoFocus={initialFocus === "name"}
onChange={(e) => setName(e.currentTarget.value)}
/>
<Textarea
placeholder="Config description"
value={description ?? undefined}
onKeyDown={onHandleEnter}
autoFocus={initialFocus === "description"}
onChange={(e) => setDescription(e.currentTarget.value)}
autosize
minRows={2}
/>
</>
) : (
<div>
<Title
ref={nameDisplayRef}
onClick={onClickEdit}
className={classes.hoverContainer}
>
{name}
</Title>
<Text
ref={descriptionDisplayRef}
onClick={onClickEdit}
style={{ whiteSpace: "pre-wrap" }}
className={classes.hoverContainer}
>
{description}
</Text>
</div>
)}
</Stack>
);
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import PromptContainer from "./prompt/PromptContainer";
import {
Container,
Group,
Button,
createStyles,
Stack,
Flex,
} from "@mantine/core";
import { Container, Button, createStyles, Stack, Flex } from "@mantine/core";
import { showNotification } from "@mantine/notifications";
import {
AIConfig,
Expand All @@ -32,6 +25,8 @@ import { debounce, uniqueId } from "lodash";
import PromptMenuButton from "./prompt/PromptMenuButton";
import GlobalParametersContainer from "./GlobalParametersContainer";
import AIConfigContext from "./AIConfigContext";
import ConfigNameDescription from "./ConfigNameDescription";
import { DEBOUNCE_MS } from "../utils/constants";

type Props = {
aiconfig: AIConfig;
Expand All @@ -48,6 +43,8 @@ export type AIConfigCallbacks = {
getModels: (search: string) => Promise<string[]>;
runPrompt: (promptName: string) => Promise<{ aiconfig: AIConfig }>;
save: (aiconfig: AIConfig) => Promise<void>;
setConfigDescription: (description: string) => Promise<void>;
setConfigName: (name: string) => Promise<void>;
updateModel: (
promptName?: string,
modelData?: string | ModelMetadata
Expand Down Expand Up @@ -124,7 +121,7 @@ export default function EditorContainer({
debounce(
(promptName: string, newPrompt: Prompt) =>
updatePromptCallback(promptName, newPrompt),
250
DEBOUNCE_MS
),
[updatePromptCallback]
);
Expand Down Expand Up @@ -189,7 +186,7 @@ export default function EditorContainer({
debounce(
(promptName?: string, modelMetadata?: string | ModelMetadata) =>
updateModelCallback(promptName, modelMetadata),
250
DEBOUNCE_MS
),
[updateModelCallback]
);
Expand Down Expand Up @@ -379,6 +376,44 @@ export default function EditorContainer({
[runPromptCallback]
);

const setNameCallback = callbacks.setConfigName;
const debouncedSetName = useMemo(
() => debounce((name: string) => setNameCallback(name), DEBOUNCE_MS),
[setNameCallback]
);

const onSetName = useCallback(
async (name: string) => {
dispatch({
type: "SET_NAME",
name,
});
await debouncedSetName(name);
},
[debouncedSetName]
);

const setDescriptionCallback = callbacks.setConfigDescription;
const debouncedSetDescription = useMemo(
() =>
debounce(
(description: string) => setDescriptionCallback(description),
DEBOUNCE_MS
),
[setDescriptionCallback]
);

const onSetDescription = useCallback(
async (description: string) => {
dispatch({
type: "SET_DESCRIPTION",
description,
});
await debouncedSetDescription(description);
},
[debouncedSetDescription]
);

const { classes } = useStyles();

const getState = useCallback(() => stateRef.current, []);
Expand All @@ -392,14 +427,17 @@ export default function EditorContainer({
return (
<AIConfigContext.Provider value={contextValue}>
<Container maw="80rem">
<Group grow m="sm">
{/* <Text sx={{ textOverflow: "ellipsis", overflow: "hidden" }} size={14}>
{path || "No path specified"}
</Text> */}
<Button loading={isSaving} ml="lg" onClick={onSave}>
<Flex justify="flex-end" mt="md" mb="xs">
<Button loading={isSaving} onClick={onSave}>
Save
</Button>
</Group>
</Flex>
<ConfigNameDescription
name={aiconfigState.name}
description={aiconfigState.description}
setDescription={onSetDescription}
setName={onSetName}
/>
</Container>
<GlobalParametersContainer
initialValue={aiconfigState?.metadata?.parameters ?? {}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export type MutateAIConfigAction =
| AddPromptAction
| DeletePromptAction
| RunPromptAction
| SetDescriptionAction
| SetNameAction
| UpdatePromptInputAction
| UpdatePromptNameAction
| UpdatePromptModelAction
Expand Down Expand Up @@ -39,6 +41,16 @@ export type RunPromptAction = {
id: string;
};

export type SetDescriptionAction = {
type: "SET_DESCRIPTION";
description: string;
};

export type SetNameAction = {
type: "SET_NAME";
name: string;
};

export type UpdatePromptInputAction = {
type: "UPDATE_PROMPT_INPUT";
id: string;
Expand Down Expand Up @@ -192,6 +204,18 @@ export default function aiconfigReducer(
},
}));
}
case "SET_DESCRIPTION": {
return {
...state,
description: action.description,
};
}
case "SET_NAME": {
return {
...state,
name: action.name,
};
}
case "UPDATE_PROMPT_INPUT": {
return reduceReplaceInput(state, action.id, () => action.input);
}
Expand Down
2 changes: 2 additions & 0 deletions python/src/aiconfig/editor/client/src/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const ROUTE_TABLE = {
ADD_PROMPT: urlJoin(API_ENDPOINT, "/add_prompt"),
DELETE_PROMPT: urlJoin(API_ENDPOINT, "/delete_prompt"),
SAVE: urlJoin(API_ENDPOINT, "/save"),
SET_DESCRIPTION: urlJoin(API_ENDPOINT, "/set_description"),
SET_NAME: urlJoin(API_ENDPOINT, "/set_name"),
LOAD: urlJoin(API_ENDPOINT, "/load"),
LIST_MODELS: urlJoin(API_ENDPOINT, "/list_models"),
RUN_PROMPT: urlJoin(API_ENDPOINT, "/run"),
Expand Down
1 change: 1 addition & 0 deletions python/src/aiconfig/editor/client/src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DEBOUNCE_MS = 250;

0 comments on commit d114595

Please sign in to comment.