From 2bda80c4e09c023ebefd8ca3b61be2356006599e Mon Sep 17 00:00:00 2001 From: Ryan Holinshead <> Date: Thu, 28 Dec 2023 22:09:16 -0500 Subject: [PATCH 1/2] [editor] Model Selector # [editor] Model Selector Implement the model selector and add a placeholder call to the future api endpoint for updating the model. https://github.com/lastmile-ai/aiconfig/assets/5060851/812f3a10-aaac-4ec3-9f1f-4d21ebc99e9f --- .../src/aiconfig/editor/client/src/Editor.tsx | 13 ++- .../client/src/components/EditorContainer.tsx | 57 ++++++++++++- .../client/src/components/aiconfigReducer.ts | 21 +++++ .../src/components/prompt/AddPromptButton.tsx | 22 +---- .../src/components/prompt/ModelSelector.tsx | 82 +++++++++++++++++++ .../src/components/prompt/PromptContainer.tsx | 21 ++++- .../editor/client/src/hooks/useLoadModels.ts | 31 +++++++ .../aiconfig/editor/client/src/utils/api.ts | 1 + 8 files changed, 221 insertions(+), 27 deletions(-) create mode 100644 python/src/aiconfig/editor/client/src/components/prompt/ModelSelector.tsx create mode 100644 python/src/aiconfig/editor/client/src/hooks/useLoadModels.ts diff --git a/python/src/aiconfig/editor/client/src/Editor.tsx b/python/src/aiconfig/editor/client/src/Editor.tsx index 26748e394..7c1bb014e 100644 --- a/python/src/aiconfig/editor/client/src/Editor.tsx +++ b/python/src/aiconfig/editor/client/src/Editor.tsx @@ -2,7 +2,7 @@ import EditorContainer, { AIConfigCallbacks, } from "./components/EditorContainer"; import { Flex, Loader, MantineProvider } from "@mantine/core"; -import { AIConfig, Prompt } from "aiconfig"; +import { AIConfig, ModelMetadata, Prompt } from "aiconfig"; import { useCallback, useEffect, useMemo, useState } from "react"; import { ufetch } from "ufetch"; import { ROUTE_TABLE } from "./utils/api"; @@ -66,12 +66,23 @@ export default function Editor() { [] ); + const updateModel = useCallback( + async (_promptName?: string, _modelData?: string | ModelMetadata) => { + // return await ufetch.post(ROUTE_TABLE.UPDATE_MODEL, + // prompt_name: promptName, + // model_data: modelData, + // }); + }, + [] + ); + const callbacks: AIConfigCallbacks = useMemo( () => ({ addPrompt, getModels, runPrompt, save, + updateModel, updatePrompt, }), [save, getModels, addPrompt, runPrompt] diff --git a/python/src/aiconfig/editor/client/src/components/EditorContainer.tsx b/python/src/aiconfig/editor/client/src/components/EditorContainer.tsx index 2536e9795..2e22c9592 100644 --- a/python/src/aiconfig/editor/client/src/components/EditorContainer.tsx +++ b/python/src/aiconfig/editor/client/src/components/EditorContainer.tsx @@ -1,7 +1,7 @@ import PromptContainer from "./prompt/PromptContainer"; import { Container, Group, Button, createStyles, Stack } from "@mantine/core"; import { showNotification } from "@mantine/notifications"; -import { AIConfig, Prompt, PromptInput } from "aiconfig"; +import { AIConfig, ModelMetadata, Prompt, PromptInput } from "aiconfig"; import { useCallback, useMemo, useReducer, useRef, useState } from "react"; import aiconfigReducer, { AIConfigReducerAction } from "./aiconfigReducer"; import { @@ -28,6 +28,10 @@ export type AIConfigCallbacks = { getModels: (search: string) => Promise; runPrompt: (promptName: string) => Promise; save: (aiconfig: AIConfig) => Promise; + updateModel: ( + promptName?: string, + modelData?: string | ModelMetadata + ) => Promise; updatePrompt: ( promptName: string, promptData: Prompt @@ -150,6 +154,16 @@ export default function EditorContainer({ [dispatch] ); + const debouncedUpdateModel = useMemo( + () => + debounce( + (promptName?: string, modelMetadata?: string | ModelMetadata) => + callbacks.updateModel(promptName, modelMetadata), + 250 + ), + [callbacks.updateModel] + ); + const onUpdatePromptModelSettings = useCallback( async (promptIndex: number, newModelSettings: any) => { dispatch({ @@ -157,11 +171,46 @@ export default function EditorContainer({ index: promptIndex, modelSettings: newModelSettings, }); - // TODO: Call server-side endpoint to update model settings + // TODO: Call server-side endpoint to update model }, [dispatch] ); + const onUpdatePromptModel = useCallback( + async (promptIndex: number, newModel?: string) => { + dispatch({ + type: "UPDATE_PROMPT_MODEL", + index: promptIndex, + modelName: newModel, + }); + + try { + const prompt = clientPromptToAIConfigPrompt( + aiconfigState.prompts[promptIndex] + ); + const currentModel = prompt.metadata?.model; + let modelData: string | ModelMetadata | undefined = newModel; + if (newModel && currentModel && typeof currentModel !== "string") { + modelData = { + ...currentModel, + name: newModel, + }; + } + + await debouncedUpdateModel(prompt.name, modelData); + + // TODO: Consolidate + } catch (err: any) { + showNotification({ + title: "Error updating prompt model", + message: err.message, + color: "red", + }); + } + }, + [dispatch, debouncedUpdateModel] + ); + const onUpdatePromptParameters = useCallback( async (promptIndex: number, newParameters: any) => { dispatch({ @@ -241,8 +290,6 @@ export default function EditorContainer({ const { classes } = useStyles(); - // TODO: Implement editor context for callbacks, readonly state, etc. - return ( <> @@ -262,9 +309,11 @@ export default function EditorContainer({ ({ + ...prompt, + metadata: { + ...prompt.metadata, + model: action.modelName + ? { + name: action.modelName, + // TODO: Consolidate settings based on schema union + } + : undefined, + }, + })); + } case "UPDATE_PROMPT_MODEL_SETTINGS": { return reduceReplacePrompt(state, action.index, (prompt) => ({ ...prompt, diff --git a/python/src/aiconfig/editor/client/src/components/prompt/AddPromptButton.tsx b/python/src/aiconfig/editor/client/src/components/prompt/AddPromptButton.tsx index cd18f0a07..d15380eb6 100644 --- a/python/src/aiconfig/editor/client/src/components/prompt/AddPromptButton.tsx +++ b/python/src/aiconfig/editor/client/src/components/prompt/AddPromptButton.tsx @@ -1,7 +1,7 @@ import { ActionIcon, Menu, TextInput } from "@mantine/core"; import { IconPlus, IconSearch, IconTextCaption } from "@tabler/icons-react"; -import { memo, useCallback, useEffect, useState } from "react"; -import { showNotification } from "@mantine/notifications"; +import { memo, useCallback, useState } from "react"; +import useLoadModels from "../../hooks/useLoadModels"; type Props = { addPrompt: (prompt: string) => void; @@ -41,30 +41,14 @@ function ModelMenuItems({ export default memo(function AddPromptButton({ addPrompt, getModels }: Props) { const [modelSearch, setModelSearch] = useState(""); - const [models, setModels] = useState([]); const [isOpen, setIsOpen] = useState(false); - const loadModels = useCallback(async (modelSearch: string) => { - try { - const models = await getModels(modelSearch); - setModels(models); - } catch (err: any) { - showNotification({ - title: "Error loading models", - message: err?.message, - color: "red", - }); - } - }, []); - const onAddPrompt = useCallback((model: string) => { addPrompt(model); setIsOpen(false); }, []); - useEffect(() => { - loadModels(modelSearch); - }, [loadModels, modelSearch]); + const models = useLoadModels(modelSearch, getModels); return ( Promise; + onSetModel: (model?: string) => void; + defaultConfigModelName?: string; +}; + +export default memo(function ModelSelector({ + prompt, + getModels, + onSetModel, + defaultConfigModelName, +}: Props) { + const [selectedModel, setSelectedModel] = useState( + getPromptModelName(prompt, defaultConfigModelName) + ); + const [showAll, setShowAll] = useState(true); + const [autocompleteSearch, setAutocompleteSearch] = useState( + getPromptModelName(prompt, defaultConfigModelName) + ); + + const models = useLoadModels(showAll ? "" : autocompleteSearch, getModels); + + const onSelectModel = (model?: string) => { + setSelectedModel(model); + onSetModel(model); + }; + + return ( + { + onSelectModel(undefined); + setShowAll(true); + setAutocompleteSearch(""); + }} + > + + + ) : null + } + filter={(searchValue: string, item: AutocompleteItem) => { + if (showAll) { + return true; + } + + const modelName: string = item.value; + return modelName + .toLocaleLowerCase() + .includes(searchValue.toLocaleLowerCase().trim()); + }} + data={models} + value={autocompleteSearch} + onChange={(value: string) => { + setAutocompleteSearch(value); + setShowAll(false); + onSelectModel(value); + models.some((model) => { + if (model === value) { + setShowAll(true); + return true; + } + }); + }} + /> + ); +}); diff --git a/python/src/aiconfig/editor/client/src/components/prompt/PromptContainer.tsx b/python/src/aiconfig/editor/client/src/components/prompt/PromptContainer.tsx index de38a03ed..80707037b 100644 --- a/python/src/aiconfig/editor/client/src/components/prompt/PromptContainer.tsx +++ b/python/src/aiconfig/editor/client/src/components/prompt/PromptContainer.tsx @@ -2,23 +2,26 @@ import PromptActionBar from "./PromptActionBar"; import PromptInputRenderer from "./prompt_input/PromptInputRenderer"; import PromptOutputsRenderer from "./prompt_outputs/PromptOutputsRenderer"; import { ClientPrompt } from "../../shared/types"; -import { getPromptModelName, getPromptSchema } from "../../utils/promptUtils"; -import { Flex, Card, Text, createStyles } from "@mantine/core"; +import { getPromptSchema } from "../../utils/promptUtils"; +import { Flex, Card, createStyles } from "@mantine/core"; import { PromptInput as AIConfigPromptInput } from "aiconfig"; import { memo, useCallback } from "react"; import { ParametersArray } from "../ParametersRenderer"; import PromptOutputBar from "./PromptOutputBar"; import PromptName from "./PromptName"; +import ModelSelector from "./ModelSelector"; type Props = { index: number; prompt: ClientPrompt; + getModels: (search: string) => Promise; onChangePromptInput: ( promptIndex: number, newPromptInput: AIConfigPromptInput ) => void; onChangePromptName: (promptIndex: number, newName: string) => void; onRunPrompt(promptIndex: number): Promise; + onUpdateModel: (promptIndex: number, newModel?: string) => void; onUpdateModelSettings: (promptIndex: number, newModelSettings: any) => void; onUpdateParameters: (promptIndex: number, newParameters: any) => void; defaultConfigModelName?: string; @@ -42,10 +45,12 @@ const useStyles = createStyles((theme) => ({ export default memo(function PromptContainer({ prompt, index, + getModels, onChangePromptInput, onChangePromptName, defaultConfigModelName, onRunPrompt, + onUpdateModel, onUpdateModelSettings, onUpdateParameters, }: Props) { @@ -87,6 +92,11 @@ export default memo(function PromptContainer({ [index, onRunPrompt] ); + const updateModel = useCallback( + (model?: string) => onUpdateModel(index, model), + [index, onUpdateModel] + ); + // TODO: When adding support for custom PromptContainers, implement a PromptContainerRenderer which // will take in the index and callback and render the appropriate PromptContainer with new memoized // callback and not having to pass index down to PromptContainer @@ -102,7 +112,12 @@ export default memo(function PromptContainer({ - {getPromptModelName(prompt, defaultConfigModelName)} + Promise +) { + const [models, setModels] = useState([]); + + const loadModels = useCallback( + async (modelSearch: string) => { + try { + const models = await getModels(modelSearch); + setModels(models); + } catch (err: any) { + showNotification({ + title: "Error loading models", + message: err?.message, + color: "red", + }); + } + }, + [getModels] + ); + + useEffect(() => { + loadModels(modelSearch); + }, [loadModels, modelSearch]); + + return models; +} diff --git a/python/src/aiconfig/editor/client/src/utils/api.ts b/python/src/aiconfig/editor/client/src/utils/api.ts index 9959e039f..bc300542d 100644 --- a/python/src/aiconfig/editor/client/src/utils/api.ts +++ b/python/src/aiconfig/editor/client/src/utils/api.ts @@ -14,5 +14,6 @@ export const ROUTE_TABLE = { LOAD: urlJoin(API_ENDPOINT, "/load"), LIST_MODELS: urlJoin(API_ENDPOINT, "/list_models"), RUN_PROMPT: urlJoin(API_ENDPOINT, "/run"), + UPDATE_MODEL: urlJoin(API_ENDPOINT, "/update_model"), UPDATE_PROMPT: urlJoin(API_ENDPOINT, "/update_prompt"), }; From 9b7bee7328c7cbe697718dac4238cc7d9d930d16 Mon Sep 17 00:00:00 2001 From: Ryan Holinshead <> Date: Thu, 28 Dec 2023 22:09:16 -0500 Subject: [PATCH 2/2] [editor] Support Deleting Prompt # [editor] Support Deleting Prompt Add the UI side for deleting a prompt, with placeholder to call the server endpoint once it's implemented https://github.com/lastmile-ai/aiconfig/assets/5060851/b3e682dd-a3ed-45c1-b153-b4b6e2094616 --- .../src/aiconfig/editor/client/src/Editor.tsx | 7 ++ .../client/src/components/EditorContainer.tsx | 67 +++++++++++++++---- .../client/src/components/aiconfigReducer.ts | 13 ++++ .../src/components/prompt/PromptContainer.tsx | 2 +- .../components/prompt/PromptMenuButton.tsx | 44 ++++++++++++ .../client/src/utils/aiconfigStateUtils.ts | 8 +++ .../aiconfig/editor/client/src/utils/api.ts | 1 + 7 files changed, 127 insertions(+), 15 deletions(-) create mode 100644 python/src/aiconfig/editor/client/src/components/prompt/PromptMenuButton.tsx diff --git a/python/src/aiconfig/editor/client/src/Editor.tsx b/python/src/aiconfig/editor/client/src/Editor.tsx index 7c1bb014e..7fcef67d1 100644 --- a/python/src/aiconfig/editor/client/src/Editor.tsx +++ b/python/src/aiconfig/editor/client/src/Editor.tsx @@ -50,6 +50,12 @@ export default function Editor() { [] ); + const deletePrompt = useCallback(async (promptName: string) => { + return await ufetch.post(ROUTE_TABLE.DELETE_PROMPT, { + prompt_name: promptName, + }); + }, []); + const runPrompt = useCallback(async (promptName: string) => { return await ufetch.post(ROUTE_TABLE.RUN_PROMPT, { prompt_name: promptName, @@ -79,6 +85,7 @@ export default function Editor() { const callbacks: AIConfigCallbacks = useMemo( () => ({ addPrompt, + deletePrompt, getModels, runPrompt, save, diff --git a/python/src/aiconfig/editor/client/src/components/EditorContainer.tsx b/python/src/aiconfig/editor/client/src/components/EditorContainer.tsx index 2e22c9592..91fd7cda8 100644 --- a/python/src/aiconfig/editor/client/src/components/EditorContainer.tsx +++ b/python/src/aiconfig/editor/client/src/components/EditorContainer.tsx @@ -1,5 +1,12 @@ import PromptContainer from "./prompt/PromptContainer"; -import { Container, Group, Button, createStyles, Stack } from "@mantine/core"; +import { + Container, + Group, + Button, + createStyles, + Stack, + Flex, +} from "@mantine/core"; import { showNotification } from "@mantine/notifications"; import { AIConfig, ModelMetadata, Prompt, PromptInput } from "aiconfig"; import { useCallback, useMemo, useReducer, useRef, useState } from "react"; @@ -11,8 +18,12 @@ import { clientPromptToAIConfigPrompt, } from "../shared/types"; import AddPromptButton from "./prompt/AddPromptButton"; -import { getDefaultNewPromptName } from "../utils/aiconfigStateUtils"; +import { + getDefaultNewPromptName, + getPrompt, +} from "../utils/aiconfigStateUtils"; import { debounce, uniqueId } from "lodash"; +import PromptMenuButton from "./prompt/PromptMenuButton"; type Props = { aiconfig: AIConfig; @@ -25,6 +36,7 @@ export type AIConfigCallbacks = { prompt: Prompt, index: number ) => Promise<{ aiconfig: AIConfig }>; + deletePrompt: (promptName: string) => Promise; getModels: (search: string) => Promise; runPrompt: (promptName: string) => Promise; save: (aiconfig: AIConfig) => Promise; @@ -272,6 +284,27 @@ export default function EditorContainer({ [callbacks.addPrompt, dispatch] ); + const onDeletePrompt = useCallback( + async (promptId: string) => { + dispatch({ + type: "DELETE_PROMPT", + id: promptId, + }); + + try { + const prompt = getPrompt(stateRef.current, promptId)!; + await callbacks.deletePrompt(prompt.name); + } catch (err: any) { + showNotification({ + title: "Error deleting prompt", + message: err.message, + color: "red", + }); + } + }, + [callbacks.deletePrompt, dispatch] + ); + const onRunPrompt = useCallback( async (promptIndex: number) => { const promptName = aiconfigState.prompts[promptIndex].name; @@ -306,18 +339,24 @@ export default function EditorContainer({ {aiconfigState.prompts.map((prompt: ClientPrompt, i: number) => { return ( - + + onDeletePrompt(prompt._ui.id)} + /> + +
prompt._ui.id !== action.id), + }; + } case "UPDATE_PROMPT_INPUT": { return reduceReplaceInput(state, action.index, () => action.input); } diff --git a/python/src/aiconfig/editor/client/src/components/prompt/PromptContainer.tsx b/python/src/aiconfig/editor/client/src/components/prompt/PromptContainer.tsx index 80707037b..454d195a5 100644 --- a/python/src/aiconfig/editor/client/src/components/prompt/PromptContainer.tsx +++ b/python/src/aiconfig/editor/client/src/components/prompt/PromptContainer.tsx @@ -107,7 +107,7 @@ export default memo(function PromptContainer({ const { classes } = useStyles(); return ( - + diff --git a/python/src/aiconfig/editor/client/src/components/prompt/PromptMenuButton.tsx b/python/src/aiconfig/editor/client/src/components/prompt/PromptMenuButton.tsx new file mode 100644 index 000000000..a73dd82fa --- /dev/null +++ b/python/src/aiconfig/editor/client/src/components/prompt/PromptMenuButton.tsx @@ -0,0 +1,44 @@ +import { Button, Menu, createStyles } from "@mantine/core"; +import { IconDotsVertical, IconTrash } from "@tabler/icons-react"; +import { memo } from "react"; + +const useStyles = createStyles((theme) => ({ + promptMenuButton: { + marginLeft: -8, + }, +})); + +export default memo(function PromptMenuButton({ + promptId, + onDeletePrompt, +}: { + promptId: string; + onDeletePrompt: (id: string) => void; +}) { + const { classes } = useStyles(); + + return ( + + + + + + + } + color="red" + onClick={() => onDeletePrompt(promptId)} + > + Delete Prompt + + + + ); +}); diff --git a/python/src/aiconfig/editor/client/src/utils/aiconfigStateUtils.ts b/python/src/aiconfig/editor/client/src/utils/aiconfigStateUtils.ts index 3e25d0a4b..56b5641bf 100644 --- a/python/src/aiconfig/editor/client/src/utils/aiconfigStateUtils.ts +++ b/python/src/aiconfig/editor/client/src/utils/aiconfigStateUtils.ts @@ -1,4 +1,5 @@ import { AIConfig } from "aiconfig"; +import { ClientAIConfig, ClientPrompt } from "../shared/types"; export function getDefaultNewPromptName(aiconfig: AIConfig): string { const existingNames = aiconfig.prompts.map((prompt) => prompt.name); @@ -8,3 +9,10 @@ export function getDefaultNewPromptName(aiconfig: AIConfig): string { } return `prompt_${i}`; } + +export function getPrompt( + aiconfig: ClientAIConfig, + id: string +): ClientPrompt | undefined { + return aiconfig.prompts.find((prompt) => prompt._ui.id === id); +} diff --git a/python/src/aiconfig/editor/client/src/utils/api.ts b/python/src/aiconfig/editor/client/src/utils/api.ts index bc300542d..44c7e49b1 100644 --- a/python/src/aiconfig/editor/client/src/utils/api.ts +++ b/python/src/aiconfig/editor/client/src/utils/api.ts @@ -10,6 +10,7 @@ const API_ENDPOINT = `${HOST_ENDPOINT}/api`; export const ROUTE_TABLE = { ADD_PROMPT: urlJoin(API_ENDPOINT, "/add_prompt"), + DELETE_PROMPT: urlJoin(API_ENDPOINT, "/delete_prompt"), SAVE: urlJoin(API_ENDPOINT, "/save"), LOAD: urlJoin(API_ENDPOINT, "/load"), LIST_MODELS: urlJoin(API_ENDPOINT, "/list_models"),