diff --git a/python/src/aiconfig/editor/client/@types/ufetch.d.ts b/python/src/aiconfig/editor/client/@types/ufetch.d.ts new file mode 100644 index 000000000..926c50e31 --- /dev/null +++ b/python/src/aiconfig/editor/client/@types/ufetch.d.ts @@ -0,0 +1,13 @@ +declare module "ufetch" { + export namespace ufetch { + function setCookie(key: string, value: string, expiry: number): void; + function getCookie(key: string): string; + + function post(path: string, data: any, options?: any); + function get(path: string, options?: any); + function put(path: string, data: any, options?: any); + function _delete(path: string, data: any, options?: any); + + export { _delete as delete, setCookie, getCookie, post, get, put }; + } +} diff --git a/python/src/aiconfig/editor/client/package.json b/python/src/aiconfig/editor/client/package.json index 702887f91..8c8e679c3 100644 --- a/python/src/aiconfig/editor/client/package.json +++ b/python/src/aiconfig/editor/client/package.json @@ -2,20 +2,6 @@ "name": "client", "version": "0.1.0", "private": true, - "dependencies": { - "@testing-library/jest-dom": "^5.17.0", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^13.5.0", - "@types/jest": "^27.5.2", - "@types/node": "^16.18.68", - "@types/react": "^18.2.45", - "@types/react-dom": "^18.2.18", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-scripts": "5.0.1", - "typescript": "^4.9.5", - "web-vitals": "^2.1.4" - }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", @@ -23,12 +9,6 @@ "test": "react-scripts test", "eject": "react-scripts eject" }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] - }, "browserslist": { "production": [ ">0.2%", @@ -40,5 +20,36 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "dependencies": { + "@emotion/react": "^11.11.1", + "@mantine/carousel": "^6.0.7", + "@mantine/core": "^6.0.7", + "@mantine/dates": "^6.0.16", + "@mantine/dropzone": "^6.0.7", + "@mantine/form": "^6.0.7", + "@mantine/hooks": "^6.0.7", + "@mantine/modals": "^6.0.7", + "@mantine/notifications": "^6.0.7", + "@mantine/prism": "^6.0.7", + "@tabler/icons-react": "^2.44.0", + "aiconfig": "^1.1.0", + "lodash": "^4.17.21", + "node-fetch": "^3.3.2", + "react": "^18", + "react-dom": "^18", + "react-markdown": "^8.0.6", + "remark-gfm": "^4.0.0", + "ufetch": "^1.6.0", + "react-scripts": "5.0.1" + }, + "devDependencies": { + "@types/lodash": "^4.14.202", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.0.2", + "typescript": "^5" } -} +} \ No newline at end of file diff --git a/python/src/aiconfig/editor/client/public/favicon.ico b/python/src/aiconfig/editor/client/public/favicon.ico deleted file mode 100644 index a11777cc4..000000000 Binary files a/python/src/aiconfig/editor/client/public/favicon.ico and /dev/null differ diff --git a/python/src/aiconfig/editor/client/public/index.html b/python/src/aiconfig/editor/client/public/index.html index aa069f27c..94cfd9c4f 100644 --- a/python/src/aiconfig/editor/client/public/index.html +++ b/python/src/aiconfig/editor/client/public/index.html @@ -24,7 +24,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + AIConfig Editor diff --git a/python/src/aiconfig/editor/client/public/logo192.png b/python/src/aiconfig/editor/client/public/logo192.png deleted file mode 100644 index fc44b0a37..000000000 Binary files a/python/src/aiconfig/editor/client/public/logo192.png and /dev/null differ diff --git a/python/src/aiconfig/editor/client/public/logo512.png b/python/src/aiconfig/editor/client/public/logo512.png deleted file mode 100644 index a4e47a654..000000000 Binary files a/python/src/aiconfig/editor/client/public/logo512.png and /dev/null differ diff --git a/python/src/aiconfig/editor/client/public/manifest.json b/python/src/aiconfig/editor/client/public/manifest.json index 080d6c77a..252a650c1 100644 --- a/python/src/aiconfig/editor/client/public/manifest.json +++ b/python/src/aiconfig/editor/client/public/manifest.json @@ -1,25 +1,8 @@ { - "short_name": "React App", - "name": "Create React App Sample", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], + "short_name": "AIConfig Editor", + "name": "Editor for AIConfig JSON files", "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" -} +} \ No newline at end of file diff --git a/python/src/aiconfig/editor/client/src/App.css b/python/src/aiconfig/editor/client/src/App.css deleted file mode 100644 index 74b5e0534..000000000 --- a/python/src/aiconfig/editor/client/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/python/src/aiconfig/editor/client/src/App.test.tsx b/python/src/aiconfig/editor/client/src/App.test.tsx deleted file mode 100644 index 2a68616d9..000000000 --- a/python/src/aiconfig/editor/client/src/App.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/python/src/aiconfig/editor/client/src/App.tsx b/python/src/aiconfig/editor/client/src/App.tsx deleted file mode 100644 index a53698aab..000000000 --- a/python/src/aiconfig/editor/client/src/App.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import logo from './logo.svg'; -import './App.css'; - -function App() { - return ( -
-
- logo -

- Edit src/App.tsx and save to reload. -

- - Learn React - -
-
- ); -} - -export default App; diff --git a/python/src/aiconfig/editor/client/src/Editor.tsx b/python/src/aiconfig/editor/client/src/Editor.tsx new file mode 100644 index 000000000..f75bd2360 --- /dev/null +++ b/python/src/aiconfig/editor/client/src/Editor.tsx @@ -0,0 +1,48 @@ +import EditorContainer from "./components/EditorContainer"; +import { ClientAIConfig } from "./shared/types"; +import { Flex, Loader } from "@mantine/core"; +import { AIConfig } from "aiconfig"; +import { useCallback, useEffect, useState } from "react"; +import { ufetch } from "ufetch"; + +export default function Editor() { + const [aiconfig, setAiConfig] = useState(); + + const loadConfig = useCallback(async () => { + const res = await ufetch.post(`/api/load`, {}); + + setAiConfig(res.aiconfig); + }, []); + + useEffect(() => { + loadConfig(); + }, [loadConfig]); + + const onBackNavigation = useCallback(() => { + // TODO: Handle file back navigation + }, []); + + const onSave = useCallback(async (aiconfig: AIConfig) => { + const res = await ufetch.post(`/api/aiconfig/save`, { + // path: file path, + aiconfig, + }); + return res; + }, []); + + return ( +
+ {!aiconfig ? ( + + + + ) : ( + + )} +
+ ); +} diff --git a/python/src/aiconfig/editor/client/src/components/EditorContainer.tsx b/python/src/aiconfig/editor/client/src/components/EditorContainer.tsx new file mode 100644 index 000000000..d62486483 --- /dev/null +++ b/python/src/aiconfig/editor/client/src/components/EditorContainer.tsx @@ -0,0 +1,109 @@ +import PromptContainer from "./prompt/PromptContainer"; +import { Container, Group, Button, createStyles } from "@mantine/core"; +import { showNotification } from "@mantine/notifications"; +import { AIConfig, PromptInput } from "aiconfig"; +import { useCallback, useReducer, useState } from "react"; +import aiconfigReducer from "./aiconfigReducer"; +import { ClientAIConfig, clientConfigToAIConfig } from "../shared/types"; + +type Props = { + aiconfig: ClientAIConfig; + onBackNavigation: () => void; + onSave: (aiconfig: AIConfig) => Promise; +}; + +const useStyles = createStyles((theme) => ({ + promptsContainer: { + [theme.fn.smallerThan("sm")]: { + padding: "0 0 200px 0", + }, + paddingBottom: 400, + }, +})); + +export default function EditorContainer({ + aiconfig: initialAIConfig, + onBackNavigation, + onSave, +}: Props) { + const [isSaving, setIsSaving] = useState(false); + const [aiconfigState, dispatch] = useReducer( + aiconfigReducer, + initialAIConfig + ); + + const save = useCallback(async () => { + setIsSaving(true); + try { + await onSave(clientConfigToAIConfig(aiconfigState)); + } catch (err: any) { + showNotification({ + title: "Error saving", + message: err.message, + color: "red", + }); + } finally { + setIsSaving(false); + } + }, [aiconfigState, onSave]); + + const onChangePromptInput = useCallback( + async (promptIndex: number, newPromptInput: PromptInput) => { + dispatch({ + type: "UPDATE_PROMPT_INPUT", + 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] + ); + + const { classes } = useStyles(); + + // TODO: Implement editor context for callbacks, readonly state, etc. + + return ( + <> + + + + {/* + {path || "No path specified"} + */} + + + + + {aiconfigState.prompts.map((prompt: any, i: number) => { + return ( + + ); + })} + + + ); +} diff --git a/python/src/aiconfig/editor/client/src/components/SettingsPropertyRenderer.tsx b/python/src/aiconfig/editor/client/src/components/SettingsPropertyRenderer.tsx new file mode 100644 index 000000000..510fcd906 --- /dev/null +++ b/python/src/aiconfig/editor/client/src/components/SettingsPropertyRenderer.tsx @@ -0,0 +1,418 @@ +import { + Text, + Group, + Stack, + Autocomplete, + Tooltip, + NumberInput, + TextInput, + Slider, + Checkbox, + ActionIcon, + Textarea, + AutocompleteItem, + Select, +} from "@mantine/core"; +import { useState, useCallback, useRef } from "react"; +import { uniqueId } from "lodash"; +import { IconHelp, IconPlus, IconTrash } from "@tabler/icons-react"; +import UnionPropertyControl, { + UnionProperty, +} from "./property_controls/UnionPropertyControl"; + +type StateSetFromPrevFn = (prev: any) => void; +export type SetStateFn = (val: StateSetFromPrevFn | any) => void; + +export type PropertyRendererProps = { + propertyName: string; + property: { [key: string]: any }; + isRequired?: boolean; + initialValue?: any; + setValue: SetStateFn; +}; + +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 = null, + setValue, +}: PropertyRendererProps) { + const propertyType = property.type; + const defaultValue = property.default; + const propertyDescription = property.description; + const [propertyValue, setPropertyValue] = useState( + initialValue ?? defaultValue + ); + + let propertyControl; + + const setAndPropagateValue = useCallback( + (newValue: ((prev: any) => void) | any) => { + const valueToSet = + typeof newValue === "function" ? newValue(propertyValue) : newValue; + + if (propertyName != null && propertyName.trim() !== "") { + setValue((prevValue: any) => ({ + ...(prevValue && typeof prevValue === "object" ? prevValue : {}), + [propertyName]: valueToSet, + })); + } else { + setValue(valueToSet); + } + + setPropertyValue(valueToSet); + }, + [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); + setAndPropagateValue(Array.from(itemValues.current.values())); + }, + [setAndPropagateValue] + ); + + const addItemToList = useCallback(async () => { + const key = uniqueId(); + setItemControls((prevItemControls) => [ + ...prevItemControls, + + { + itemValues.current.set(key, newItem); + setAndPropagateValue(Array.from(itemValues.current.values())); + }} + /> + removeItemFromList(key)}> + + + , + ]); + }, [property.items, removeItemFromList, setAndPropagateValue]); + + 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 ?? "select"} + data={property.enum} + value={propertyValue ?? ""} + onChange={setAndPropagateValue} + /> + ); + } else { + propertyControl = ( + + } + placeholder={propertyValue} + required={isRequired} + withAsterisk={isRequired} + radius="md" + value={propertyValue ?? ""} + onChange={(event) => + setAndPropagateValue(event.currentTarget.value) + } + /> + ); + } + break; + } + case "text": { + propertyControl = ( +