diff --git a/cli/aiconfig-editor/package.json b/cli/aiconfig-editor/package.json index ae3dcc152..94430b50e 100644 --- a/cli/aiconfig-editor/package.json +++ b/cli/aiconfig-editor/package.json @@ -25,6 +25,7 @@ "@mantine/tiptap": "^6.0.7", "@tabler/icons-react": "^2.44.0", "aiconfig": "^1.1.0", + "lodash": "^4.17.21", "next": "14.0.2", "node-fetch": "^3.3.2", "react": "^18", @@ -34,6 +35,7 @@ "ufetch": "^1.6.0" }, "devDependencies": { + "@types/lodash": "^4.14.202", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", @@ -41,4 +43,4 @@ "eslint-config-next": "14.0.2", "typescript": "^5" } -} \ No newline at end of file +} diff --git a/cli/aiconfig-editor/pages/api/aiconfig/save.ts b/cli/aiconfig-editor/pages/api/aiconfig/save.ts index 29252ab36..30d05ddad 100644 --- a/cli/aiconfig-editor/pages/api/aiconfig/save.ts +++ b/cli/aiconfig-editor/pages/api/aiconfig/save.ts @@ -1,6 +1,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { ErrorResponse } from "@/src/shared/serverTypes"; -import { AIConfig, AIConfigRuntime } from "aiconfig"; +import { AIConfig, AIConfigRuntime, Output } from "aiconfig"; +import { ClientAIConfig } from "@/src/shared/types"; type Data = { status: string; @@ -8,7 +9,7 @@ type Data = { type RequestBody = { path: string; - aiconfig: AIConfig; + aiconfig: ClientAIConfig; }; export default async function handler( @@ -29,9 +30,27 @@ export default async function handler( return res.status(500).json({ error: "No aiconfig data provided" }); } + // TODO: Once ouputs are properly structured, remove this and use body.aiconfig directly + const clientAIConfig = body.aiconfig; + const config = { + ...clientAIConfig, + prompts: clientAIConfig.prompts.map((prompt) => ({ + ...prompt, + outputs: prompt.outputs?.map((output) => { + if (output.output_type === "execute_result") { + const outputWithoutRenderData = { ...output, renderData: undefined }; + delete outputWithoutRenderData.renderData; + return outputWithoutRenderData as Output; + } else { + return output as Output; + } + }), + })), + }; + // Construct the config and ensure proper serialization for saving - const config = await AIConfigRuntime.loadJSON(body.aiconfig); - config.save(body.path, { serializeOutputs: true }); + const serializedConfig = await AIConfigRuntime.loadJSON(config); + serializedConfig.save(body.path, { serializeOutputs: true }); res.status(200).json({ status: "ok" }); } diff --git a/cli/aiconfig-editor/src/components/EditorContainer.tsx b/cli/aiconfig-editor/src/components/EditorContainer.tsx index 2ce70ff67..2a3180ebd 100644 --- a/cli/aiconfig-editor/src/components/EditorContainer.tsx +++ b/cli/aiconfig-editor/src/components/EditorContainer.tsx @@ -49,12 +49,25 @@ export default function EditorContainer({ }, [aiconfigState, onSave]); const onChangePromptInput = useCallback( - (i: number, newPromptInput: PromptInput) => { + async (promptIndex: number, newPromptInput: PromptInput) => { dispatch({ type: "UPDATE_PROMPT_INPUT", - index: i, + index: promptIndex, input: newPromptInput, }); + // TODO: Call server-side endpoint to update prompt input + }, + [dispatch] + ); + + const onUpdatePromptModelSettings = useCallback( + async (promptIndex: number, newModelSettings: any) => { + dispatch({ + type: "UPDATE_PROMPT_MODEL_SETTINGS", + index: promptIndex, + modelSettings: newModelSettings, + }); + // TODO: Call server-side endpoint to update model settings }, [dispatch] ); @@ -86,6 +99,7 @@ export default function EditorContainer({ prompt={prompt} key={prompt.name} onChangePromptInput={onChangePromptInput} + onUpdateModelSettings={onUpdatePromptModelSettings} defaultConfigModelName={aiconfigState.metadata.default_model} /> ); diff --git a/cli/aiconfig-editor/src/components/SettingsPropertyRenderer.tsx b/cli/aiconfig-editor/src/components/SettingsPropertyRenderer.tsx index 55d206c5c..82e793e4e 100644 --- a/cli/aiconfig-editor/src/components/SettingsPropertyRenderer.tsx +++ b/cli/aiconfig-editor/src/components/SettingsPropertyRenderer.tsx @@ -1,25 +1,386 @@ -import { Flex } from "@mantine/core"; -import { memo } from "react"; +import { + Text, + Group, + Stack, + Autocomplete, + Tooltip, + NumberInput, + TextInput, + Slider, + Checkbox, + ActionIcon, + Textarea, + AutocompleteItem, + Select, +} from "@mantine/core"; +import { useState, useCallback, useEffect, useMemo, memo, useRef } from "react"; +import { uniqueId } from "lodash"; +import { IconHelp, IconPlus, IconTrash } from "@tabler/icons-react"; type Props = { propertyName: string; property: { [key: string]: any }; isRequired?: boolean; initialValue: any; + setValue: (value: any) => void; }; -export default memo(function SettingsPropertyRenderer({ +export function PropertyLabel(props: { + propertyName: string; + propertyDescription: string; +}) { + const { propertyName, propertyDescription } = props; + return propertyDescription != null && propertyDescription.trim() !== "" ? ( + + {propertyName} + + + + + + + + ) : ( + {propertyName} + ); +} + +export default function SettingsPropertyRenderer({ propertyName, property, isRequired = false, initialValue, + setValue, }: Props) { - return ( - -
{propertyName}
-
{JSON.stringify(property)}
-
isRequired: {JSON.stringify(isRequired)}
-
initialValue: {JSON.stringify(initialValue)}
-
+ const propertyType = property.type; + const defaultValue = property.default; + const propertyDescription = property.description; + const [propertyValue, setPropertyValue] = useState( + initialValue ?? defaultValue ); -}); + + let propertyControl; + + useEffect(() => { + if (propertyName != null && propertyName.trim() !== "") { + setValue((oldValue: any) => { + return { + ...(oldValue ?? {}), + [propertyName]: propertyValue, + }; + }); + } else { + setValue(propertyValue); + } + }, [propertyName, propertyValue, setValue]); + + // Used in the case the property is an array + // TODO: Should initialize with values from settings if available + const [itemControls, setItemControls] = useState([]); + const itemValues = useRef(new Map()); + + const removeItemFromList = useCallback( + async (key: string) => { + setItemControls((prevItemControls) => + prevItemControls.filter((item) => item.key !== key) + ); + + itemValues.current.delete(key); + setPropertyValue(Array.from(itemValues.current.values())); + }, + [setPropertyValue] + ); + + const addItemToList = useCallback(async () => { + const key = uniqueId(); + setItemControls((prevItemControls) => [ + ...prevItemControls, + + { + itemValues.current.set(key, newItem); + setPropertyValue(Array.from(itemValues.current.values())); + }} + /> + removeItemFromList(key)}> + + + , + ]); + }, [property, removeItemFromList]); + + switch (propertyType) { + case "string": { + if (property.enum != null) { + propertyControl = ( + + } + filter={(value: string, item: AutocompleteItem) => { + const label: string = item.value.toLocaleLowerCase(); + const val = value.toLocaleLowerCase().trim(); + + // If selected value matches enum exactly (selected case), show all options + if ( + property.enum && + property.enum.some((v: string) => v === val) + ) { + return true; + } + + // Include item if typed value is a substring + return label.includes(val); + }} + required={isRequired} + placeholder={propertyValue} + data={property.enum} + value={propertyValue ?? ""} + onChange={setPropertyValue} + /> + ); + } else { + propertyControl = ( + + } + placeholder={propertyValue} + required={isRequired} + withAsterisk={isRequired} + radius="md" + value={propertyValue ?? ""} + onChange={(event) => setPropertyValue(event.currentTarget.value)} + /> + ); + } + break; + } + case "text": { + propertyControl = ( +