diff --git a/editor/vscode/ai-workbook-ext/.eslintrc.json b/editor/vscode/ai-workbook-ext/.eslintrc.json new file mode 100644 index 000000000..86c86f379 --- /dev/null +++ b/editor/vscode/ai-workbook-ext/.eslintrc.json @@ -0,0 +1,30 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "import", + "format": [ "camelCase", "PascalCase" ] + } + ], + "@typescript-eslint/semi": "warn", + "curly": "warn", + "eqeqeq": "warn", + "no-throw-literal": "warn", + "semi": "off" + }, + "ignorePatterns": [ + "out", + "dist", + "**/*.d.ts" + ] +} \ No newline at end of file diff --git a/editor/vscode/ai-workbook-ext/.vscode-test.mjs b/editor/vscode/ai-workbook-ext/.vscode-test.mjs new file mode 100644 index 000000000..b62ba25f0 --- /dev/null +++ b/editor/vscode/ai-workbook-ext/.vscode-test.mjs @@ -0,0 +1,5 @@ +import { defineConfig } from '@vscode/test-cli'; + +export default defineConfig({ + files: 'out/test/**/*.test.js', +}); diff --git a/editor/vscode/ai-workbook-ext/.vscode/extensions.json b/editor/vscode/ai-workbook-ext/.vscode/extensions.json new file mode 100644 index 000000000..db70f8889 --- /dev/null +++ b/editor/vscode/ai-workbook-ext/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "dbaeumer.vscode-eslint", + "ms-vscode.extension-test-runner" + ] +} diff --git a/editor/vscode/ai-workbook-ext/.vscode/launch.json b/editor/vscode/ai-workbook-ext/.vscode/launch.json new file mode 100644 index 000000000..a0ca3cb93 --- /dev/null +++ b/editor/vscode/ai-workbook-ext/.vscode/launch.json @@ -0,0 +1,17 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "preLaunchTask": "${defaultBuildTask}" + } + ] +} diff --git a/editor/vscode/ai-workbook-ext/.vscode/settings.json b/editor/vscode/ai-workbook-ext/.vscode/settings.json new file mode 100644 index 000000000..8bf37e053 --- /dev/null +++ b/editor/vscode/ai-workbook-ext/.vscode/settings.json @@ -0,0 +1,12 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.exclude": { + "out": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "out": true // set this to false to include "out" folder in search results + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off", + "editor.formatOnSave": true +} \ No newline at end of file diff --git a/editor/vscode/ai-workbook-ext/.vscode/tasks.json b/editor/vscode/ai-workbook-ext/.vscode/tasks.json new file mode 100644 index 000000000..3b17e53b6 --- /dev/null +++ b/editor/vscode/ai-workbook-ext/.vscode/tasks.json @@ -0,0 +1,20 @@ +// See https://go.microsoft.com/fwlink/?LinkId=733558 +// for the documentation about the tasks.json format +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/editor/vscode/ai-workbook-ext/.vscodeignore b/editor/vscode/ai-workbook-ext/.vscodeignore new file mode 100644 index 000000000..72aa0fe2e --- /dev/null +++ b/editor/vscode/ai-workbook-ext/.vscodeignore @@ -0,0 +1,11 @@ +.vscode/** +.vscode-test/** +src/** +.gitignore +.yarnrc +vsc-extension-quickstart.md +**/tsconfig.json +**/.eslintrc.json +**/*.map +**/*.ts +**/.vscode-test.* diff --git a/editor/vscode/ai-workbook-ext/.yarnrc b/editor/vscode/ai-workbook-ext/.yarnrc new file mode 100644 index 000000000..f757a6ac5 --- /dev/null +++ b/editor/vscode/ai-workbook-ext/.yarnrc @@ -0,0 +1 @@ +--ignore-engines true \ No newline at end of file diff --git a/editor/vscode/ai-workbook-ext/CHANGELOG.md b/editor/vscode/ai-workbook-ext/CHANGELOG.md new file mode 100644 index 000000000..0ccd82bd1 --- /dev/null +++ b/editor/vscode/ai-workbook-ext/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change Log + +All notable changes to the "ai-workbook-ext" extension will be documented in this file. + +Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. + +## [Unreleased] + +- Initial release \ No newline at end of file diff --git a/editor/vscode/ai-workbook-ext/README.md b/editor/vscode/ai-workbook-ext/README.md new file mode 100644 index 000000000..1d207baf2 --- /dev/null +++ b/editor/vscode/ai-workbook-ext/README.md @@ -0,0 +1,71 @@ +# ai-workbook-ext README + +This is the README for your extension "ai-workbook-ext". After writing up a brief description, we recommend including the following sections. + +## Features + +Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file. + +For example if there is an image subfolder under your extension project workspace: + +\!\[feature X\]\(images/feature-x.png\) + +> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow. + +## Requirements + +If you have any requirements or dependencies, add a section describing those and how to install and configure them. + +## Extension Settings + +Include if your extension adds any VS Code settings through the `contributes.configuration` extension point. + +For example: + +This extension contributes the following settings: + +* `myExtension.enable`: Enable/disable this extension. +* `myExtension.thing`: Set to `blah` to do something. + +## Known Issues + +Calling out known issues can help limit users opening duplicate issues against your extension. + +## Release Notes + +Users appreciate release notes as you update your extension. + +### 1.0.0 + +Initial release of ... + +### 1.0.1 + +Fixed issue #. + +### 1.1.0 + +Added features X, Y, and Z. + +--- + +## Following extension guidelines + +Ensure that you've read through the extensions guidelines and follow the best practices for creating your extension. + +* [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines) + +## Working with Markdown + +You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts: + +* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux). +* Toggle preview (`Shift+Cmd+V` on macOS or `Shift+Ctrl+V` on Windows and Linux). +* Press `Ctrl+Space` (Windows, Linux, macOS) to see a list of Markdown snippets. + +## For more information + +* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown) +* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/) + +**Enjoy!** diff --git a/editor/vscode/ai-workbook-ext/editor/README.md b/editor/vscode/ai-workbook-ext/editor/README.md new file mode 100644 index 000000000..9fd75ea27 --- /dev/null +++ b/editor/vscode/ai-workbook-ext/editor/README.md @@ -0,0 +1,105 @@ +# Editor + +## Usage (prod) + +One liner for local testing; run this inside the root of aiconfig repo. Create a model parser registry py file to get started. + +`python -m 'aiconfig.scripts.aiconfig_cli' edit --aiconfig-path=../cookbooks/Getting-Started/travel.aiconfig.json --server-mode='prod' ` + +### Install: + +Install python-aiconfig from pip, then set `aiconfig` alias (in shell, .bashrc, .zshrc, etc.) + +One-liner: +`pip install python-aiconfig; alias aiconfig="python -m 'aiconfig.scripts.aiconfig_cli'"` + +### Run + +`aiconfig edit --aiconfig-path=/my/path'` + +``` +[INFO] 2023-12-18 23:54:14,379 server.py:32: Edit config: { + "server_port": 8080, + "aiconfig_path": "/my/path" +} +[INFO] 2023-12-18 23:54:14,379 server.py:33: Editor server running on http://localhost:8080 +``` + +Go to url in browser to use app. + +### Loading model parsers + +To use a model parser that doesn't ship with aiconfig: 0. Make sure your model parser package is installed, e.g. +`pip install python-aiconfig-llama`, or +`pip install -e path/to/my/local/parser/package` + +1. Make a Python file e.g. my_editor_plugin.py. It must define a () -> None called `register_model_parsers. + Example: + +``` +from aiconfig import AIConfigRuntime +from llama import LlamaModelParser + + +def register_model_parsers() -> None: + model_path = "/Users/jonathan/Projects/aiconfig/models/llama-2-7b-chat.Q4_K_M.gguf" + llama_model_parser = LlamaModelParser(model_path) + AIConfigRuntime.register_model_parser(llama_model_parser, "llama-2-7b-chat") +``` + +2. Run aiconfig edit server with `--parsers-module-path="/path/to/my_editor_plugin.py"` + +e.g. `aiconfig edit --parsers-module-path="/path/to/my_editor_plugin.py"` + +3. Use editor as usual. + +## Dev + +### Install: + +`pip install -e path/to/local/aiconfig/python` +`alias aiconfig="python -m 'aiconfig.scripts.aiconfig_cli'"`` + +### Run backend and frontend servers: + +(debug mode will run the react server) +`aiconfig edit --aiconfig-path=/my/path --server-port=8080 --server-mode=debug_servers` + +More info: +`aiconfig --help` +`aiconfig edit --help` + +### Frontent + +Use React server localhost:3000 + +### Backend + +Tip: use `--server-mode=debug_backend` +Server will hot reload when you save file. Recommend disabling autosave. + +Send POST requests from + +- curl (https://stackoverflow.com/questions/22947905/flask-example-with-post) +- Chrome dev tools (https://stackoverflow.com/questions/14248296/making-http-requests-using-chrome-developer-tools) +- Jupyter: + +``` +import requests +url = 'http://localhost:8080/api/add_prompt' +data = { + "prompt_name": "gen_packing_list", + "prompt_data": { + + } + } +response = requests.post(url, json=data) +print(f"{response=}"), + +import json +response_json = json.loads(response.text) +message, output = response_json['message'], response_json['output'] +print(f"{message=}") +print("output:") +print(output) +``` diff --git a/editor/vscode/ai-workbook-ext/editor/__init__.py b/editor/vscode/ai-workbook-ext/editor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/editor/vscode/ai-workbook-ext/editor/client/.gitignore b/editor/vscode/ai-workbook-ext/editor/client/.gitignore new file mode 100644 index 000000000..704a02e0a --- /dev/null +++ b/editor/vscode/ai-workbook-ext/editor/client/.gitignore @@ -0,0 +1,44 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* \ No newline at end of file diff --git a/editor/vscode/ai-workbook-ext/editor/client/@types/ufetch.d.ts b/editor/vscode/ai-workbook-ext/editor/client/@types/ufetch.d.ts new file mode 100644 index 000000000..926c50e31 --- /dev/null +++ b/editor/vscode/ai-workbook-ext/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/editor/vscode/ai-workbook-ext/editor/client/__init__.py b/editor/vscode/ai-workbook-ext/editor/client/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/editor/vscode/ai-workbook-ext/editor/client/package.json b/editor/vscode/ai-workbook-ext/editor/client/package.json new file mode 100644 index 000000000..cf32d5169 --- /dev/null +++ b/editor/vscode/ai-workbook-ext/editor/client/package.json @@ -0,0 +1,59 @@ +{ + "name": "client", + "version": "0.1.0", + "private": true, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "build:vscode": "node ./scripts/build-react-no-split.js", + "postbuild": "node postbuild.js", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "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", + "@vscode/webview-ui-toolkit": "^1.4.0", + "aiconfig": "../../../../../typescript", + "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", + "url-join": "^5.0.0" + }, + "devDependencies": { + "@types/lodash": "^4.14.202", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.0.2", + "react-scripts": "5.0.1", + "rewire": "^5.0.0", + "typescript": "^5" + } +} diff --git a/editor/vscode/ai-workbook-ext/editor/client/postbuild.js b/editor/vscode/ai-workbook-ext/editor/client/postbuild.js new file mode 100644 index 000000000..7017efbe0 --- /dev/null +++ b/editor/vscode/ai-workbook-ext/editor/client/postbuild.js @@ -0,0 +1,80 @@ +var path = require("path"); +const fs = require("fs"); + +const targetSource = "./build"; // Relative path to copy files from +const targetDestination = "../server/static"; // Relative path to copy files to + +/** + * Remove directory recursively + * @param {string} dir_path + * @see https://stackoverflow.com/a/42505874 + */ +function rimraf(dir_path) { + if (fs.existsSync(dir_path)) { + fs.readdirSync(dir_path).forEach(function (entry) { + var entry_path = path.join(dir_path, entry); + if (fs.lstatSync(entry_path).isDirectory()) { + rimraf(entry_path); + } else { + fs.unlinkSync(entry_path); + } + }); + fs.rmdirSync(dir_path); + } +} + +/** + * Copy a file + * @param {string} source + * @param {string} target + * @see https://stackoverflow.com/a/26038979 + */ +function copyFileSync(source, target) { + var targetFile = target; + // If target is a directory a new file with the same name will be created + if (fs.existsSync(target)) { + if (fs.lstatSync(target).isDirectory()) { + targetFile = path.join(target, path.basename(source)); + } + } + fs.writeFileSync(targetFile, fs.readFileSync(source)); +} + +/** + * Copy a folder recursively + * @param {string} source + * @param {string} target + * @see https://stackoverflow.com/a/26038979 + */ +function copyFolderRecursiveSync(source, target, root = false) { + var files = []; + // Check if folder needs to be created or integrated + var targetFolder = root ? target : path.join(target, path.basename(source)); + if (!fs.existsSync(targetFolder)) { + fs.mkdirSync(targetFolder); + } + // Copy + if (fs.lstatSync(source).isDirectory()) { + files = fs.readdirSync(source); + files.forEach(function (file) { + var curSource = path.join(source, file); + if (fs.lstatSync(curSource).isDirectory()) { + copyFolderRecursiveSync(curSource, targetFolder); + } else { + copyFileSync(curSource, targetFolder); + } + }); + } +} + +// Calculate absolute paths using the relative paths we defined at the top +const sourceFolder = path.resolve(targetSource); +const destinationFolder = path.resolve(targetDestination); + +// Remove destination folder if it exists to clear it +if (fs.existsSync(destinationFolder)) { + rimraf(destinationFolder); +} + +// Copy the build over +copyFolderRecursiveSync(sourceFolder, destinationFolder, true); diff --git a/editor/vscode/ai-workbook-ext/editor/client/public/index.html b/editor/vscode/ai-workbook-ext/editor/client/public/index.html new file mode 100644 index 000000000..94cfd9c4f --- /dev/null +++ b/editor/vscode/ai-workbook-ext/editor/client/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + AIConfig Editor + + + +
+ + + diff --git a/editor/vscode/ai-workbook-ext/editor/client/public/manifest.json b/editor/vscode/ai-workbook-ext/editor/client/public/manifest.json new file mode 100644 index 000000000..252a650c1 --- /dev/null +++ b/editor/vscode/ai-workbook-ext/editor/client/public/manifest.json @@ -0,0 +1,8 @@ +{ + "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/editor/vscode/ai-workbook-ext/editor/client/public/robots.txt b/editor/vscode/ai-workbook-ext/editor/client/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/editor/vscode/ai-workbook-ext/editor/client/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/editor/vscode/ai-workbook-ext/editor/client/scripts/build-react-no-split.js b/editor/vscode/ai-workbook-ext/editor/client/scripts/build-react-no-split.js new file mode 100644 index 000000000..25d064be3 --- /dev/null +++ b/editor/vscode/ai-workbook-ext/editor/client/scripts/build-react-no-split.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node + +/** + * A script that overrides some of the create-react-app build script configurations + * in order to disable code splitting/chunking and rename the output build files so + * they have no hash. (Reference: https://mtm.dev/disable-code-splitting-create-react-app). + * + * This is crucial for getting React webview code to run because VS Code expects a + * single (consistently named) JavaScript and CSS file when configuring webviews. + */ + +const rewire = require("rewire"); +const defaults = rewire("react-scripts/scripts/build.js"); +const config = defaults.__get__("config"); + +// Disable code splitting +config.optimization.splitChunks = { + cacheGroups: { + default: false, + }, +}; + +// Disable code chunks +config.optimization.runtimeChunk = false; + +// Rename main.{hash}.js to main.js +config.output.filename = "static/js/[name].js"; + +// Rename main.{hash}.css to main.css +config.plugins[5].options.filename = "static/css/[name].css"; +config.plugins[5].options.moduleFilename = () => "static/css/main.css"; diff --git a/editor/vscode/ai-workbook-ext/editor/client/src/Editor.tsx b/editor/vscode/ai-workbook-ext/editor/client/src/Editor.tsx new file mode 100644 index 000000000..b8e88bee3 --- /dev/null +++ b/editor/vscode/ai-workbook-ext/editor/client/src/Editor.tsx @@ -0,0 +1,130 @@ +import EditorContainer, { + AIConfigCallbacks, +} from "./components/EditorContainer"; +import { Flex, Loader, MantineProvider } from "@mantine/core"; +import { AIConfig, Prompt } from "aiconfig"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { ufetch } from "ufetch"; +import { ROUTE_TABLE } from "./utils/api"; + +export default function Editor() { + const [aiconfig, setAiConfig] = useState({ + name: "NYC Trip Planner", + description: "Intrepid explorer with ChatGPT and AIConfig", + schema_version: "latest", + metadata: { + models: { + "gpt-3.5-turbo": { + model: "gpt-3.5-turbo", + top_p: 1, + temperature: 1, + }, + "gpt-4": { + model: "gpt-4", + max_tokens: 3000, + system_prompt: + "You are an expert travel coordinator with exquisite taste.", + }, + }, + default_model: "gpt-3.5-turbo", + }, + prompts: [ + { + name: "get_activities", + input: "Tell me 10 fun attractions to do in NYC.", + }, + { + name: "gen_itinerary", + input: + "Generate an itinerary ordered by {{order_by}} for these activities: {{get_activities.output}}.", + metadata: { + model: "gpt-4", + parameters: { + order_by: "geographic location", + }, + }, + }, + ], + }); + + // const loadConfig = useCallback(async () => { + // const res = await ufetch.post(ROUTE_TABLE.LOAD, {}); + + // setAiConfig(res.aiconfig); + // }, []); + + // useEffect(() => { + // loadConfig(); + // }, [loadConfig]); + + const save = useCallback(async (aiconfig: AIConfig) => { + const res = await ufetch.post(ROUTE_TABLE.SAVE, { + // path: file path, + aiconfig, + }); + return res; + }, []); + + const getModels = useCallback(async (search: string) => { + // For now, rely on caching and handle client-side search filtering + // We will use server-side search filtering for Gradio + const res = await ufetch.get(ROUTE_TABLE.LIST_MODELS); + const models = res.data; + if (search && search.length > 0) { + return models.filter((model: string) => model.indexOf(search) >= 0); + } + return models; + }, []); + + const addPrompt = useCallback( + async (promptName: string, promptData: Prompt, index: number) => { + return await ufetch.post(ROUTE_TABLE.ADD_PROMPT, { + prompt_name: promptName, + prompt_data: promptData, + index, + }); + }, + [] + ); + + const runPrompt = useCallback(async (promptName: string) => { + return await ufetch.post(ROUTE_TABLE.RUN_PROMPT, { + prompt_name: promptName, + }); + }, []); + + const updatePrompt = useCallback( + async (promptName: string, promptData: Prompt) => { + return await ufetch.post(ROUTE_TABLE.UPDATE_PROMPT, { + prompt_name: promptName, + prompt_data: promptData, + }); + }, + [] + ); + + const callbacks: AIConfigCallbacks = useMemo( + () => ({ + addPrompt, + getModels, + runPrompt, + save, + updatePrompt, + }), + [save, getModels, addPrompt, runPrompt] + ); + + return ( +
+ + {!aiconfig ? ( + + + + ) : ( + + )} + +
+ ); +} diff --git a/editor/vscode/ai-workbook-ext/editor/client/src/components/EditorContainer.tsx b/editor/vscode/ai-workbook-ext/editor/client/src/components/EditorContainer.tsx new file mode 100644 index 000000000..2536e9795 --- /dev/null +++ b/editor/vscode/ai-workbook-ext/editor/client/src/components/EditorContainer.tsx @@ -0,0 +1,289 @@ +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 { useCallback, useMemo, useReducer, useRef, useState } from "react"; +import aiconfigReducer, { AIConfigReducerAction } from "./aiconfigReducer"; +import { + ClientPrompt, + aiConfigToClientConfig, + clientConfigToAIConfig, + clientPromptToAIConfigPrompt, +} from "../shared/types"; +import AddPromptButton from "./prompt/AddPromptButton"; +import { getDefaultNewPromptName } from "../utils/aiconfigStateUtils"; +import { debounce, uniqueId } from "lodash"; + +type Props = { + aiconfig: AIConfig; + callbacks: AIConfigCallbacks; +}; + +export type AIConfigCallbacks = { + addPrompt: ( + promptName: string, + prompt: Prompt, + index: number + ) => Promise<{ aiconfig: AIConfig }>; + getModels: (search: string) => Promise; + runPrompt: (promptName: string) => Promise; + save: (aiconfig: AIConfig) => Promise; + updatePrompt: ( + promptName: string, + promptData: Prompt + ) => Promise<{ aiconfig: AIConfig }>; +}; + +const useStyles = createStyles((theme) => ({ + addPromptRow: { + borderRadius: "4px", + display: "inline-block", + bottom: -24, + left: -40, + "&:hover": { + backgroundColor: + theme.colorScheme === "light" + ? theme.colors.gray[1] + : "rgba(255, 255, 255, 0.1)", + }, + [theme.fn.smallerThan("sm")]: { + marginLeft: "0", + display: "block", + position: "static", + bottom: -10, + left: 0, + height: 28, + margin: "10px 0", + }, + }, + promptsContainer: { + [theme.fn.smallerThan("sm")]: { + padding: "0 0 200px 0", + }, + paddingBottom: 400, + }, +})); + +export default function EditorContainer({ + aiconfig: initialAIConfig, + callbacks, +}: Props) { + const [isSaving, setIsSaving] = useState(false); + const [aiconfigState, dispatch] = useReducer( + aiconfigReducer, + aiConfigToClientConfig(initialAIConfig) + ); + + const stateRef = useRef(aiconfigState); + stateRef.current = aiconfigState; + + const onSave = useCallback(async () => { + setIsSaving(true); + try { + await callbacks.save(clientConfigToAIConfig(aiconfigState)); + } catch (err: any) { + showNotification({ + title: "Error saving", + message: err.message, + color: "red", + }); + } finally { + setIsSaving(false); + } + }, [aiconfigState, callbacks.save]); + + const debouncedUpdatePrompt = useMemo( + () => + debounce( + (promptName: string, newPrompt: Prompt) => + callbacks.updatePrompt(promptName, newPrompt), + 250 + ), + [callbacks.updatePrompt] + ); + + const onChangePromptInput = useCallback( + async (promptIndex: number, newPromptInput: PromptInput) => { + const action: AIConfigReducerAction = { + type: "UPDATE_PROMPT_INPUT", + index: promptIndex, + input: newPromptInput, + }; + + dispatch(action); + + try { + const prompt = clientPromptToAIConfigPrompt( + aiconfigState.prompts[promptIndex] + ); + const serverConfigRes = await debouncedUpdatePrompt(prompt.name, { + ...prompt, + input: newPromptInput, + }); + + dispatch({ + type: "CONSOLIDATE_AICONFIG", + action, + config: serverConfigRes!.aiconfig, + }); + } catch (err: any) { + showNotification({ + title: "Error adding prompt to config", + message: err.message, + color: "red", + }); + } + }, + [dispatch, debouncedUpdatePrompt] + ); + + const onChangePromptName = useCallback( + async (promptIndex: number, newName: string) => { + const action: AIConfigReducerAction = { + type: "UPDATE_PROMPT_NAME", + index: promptIndex, + name: newName, + }; + + dispatch(action); + }, + [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 onUpdatePromptParameters = useCallback( + async (promptIndex: number, newParameters: any) => { + dispatch({ + type: "UPDATE_PROMPT_PARAMETERS", + index: promptIndex, + parameters: newParameters, + }); + // TODO: Call server-side endpoint to update prompt parameters + }, + [dispatch] + ); + + const onAddPrompt = useCallback( + async (promptIndex: number, model: string) => { + const promptName = getDefaultNewPromptName( + stateRef.current as unknown as AIConfig + ); + + const newPrompt: Prompt = { + name: promptName, + input: "", // TODO: Can we use schema to get input structure, string vs object? + metadata: { + model, + }, + }; + + const action: AIConfigReducerAction = { + type: "ADD_PROMPT_AT_INDEX", + index: promptIndex, + prompt: { + ...newPrompt, + _ui: { + id: uniqueId(), + }, + }, + }; + + dispatch(action); + + try { + const serverConfigRes = await callbacks.addPrompt( + promptName, + newPrompt, + promptIndex + ); + dispatch({ + type: "CONSOLIDATE_AICONFIG", + action, + config: serverConfigRes.aiconfig, + }); + } catch (err: any) { + showNotification({ + title: "Error adding prompt to config", + message: err.message, + color: "red", + }); + } + }, + [callbacks.addPrompt, dispatch] + ); + + const onRunPrompt = useCallback( + async (promptIndex: number) => { + const promptName = aiconfigState.prompts[promptIndex].name; + try { + await callbacks.runPrompt(promptName); + } catch (err: any) { + showNotification({ + title: "Error running prompt", + message: err.message, + color: "red", + }); + } + }, + [callbacks.runPrompt] + ); + + const { classes } = useStyles(); + + // TODO: Implement editor context for callbacks, readonly state, etc. + + return ( + <> + + + {/* + {path || "No path specified"} + */} + + + + + {aiconfigState.prompts.map((prompt: ClientPrompt, i: number) => { + return ( + + +
+ + onAddPrompt( + i + 1 /* insert below current prompt index */, + model + ) + } + /> +
+
+ ); + })} +
+ + ); +} diff --git a/editor/vscode/ai-workbook-ext/editor/client/src/components/ParametersRenderer.tsx b/editor/vscode/ai-workbook-ext/editor/client/src/components/ParametersRenderer.tsx new file mode 100644 index 000000000..c214bdbe2 --- /dev/null +++ b/editor/vscode/ai-workbook-ext/editor/client/src/components/ParametersRenderer.tsx @@ -0,0 +1,244 @@ +import { + Group, + Text, + TextInput, + Textarea, + ActionIcon, + Stack, + useMantineTheme, + Tooltip, +} from "@mantine/core"; +import { IconTrash, IconPlus } from "@tabler/icons-react"; +import { debounce, uniqueId } from "lodash"; +import { useState, useCallback, memo, useMemo } from "react"; +import { JSONValue, JSONObject } from "aiconfig"; + +type Parameter = { parameterName: string; parameterValue: JSONValue }; + +/** + * Parameter name must start with a letter (a-z, A-Z) or an underscore (_). The rest of the + * name can contain letters, digits (0-9), underscores, and dollar signs ($). + */ +export function isValidParameterName(name: string): boolean { + const validNamePattern = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; + return validNamePattern.test(name); +} + +const ParameterInput = memo(function ParameterInput(props: { + onUpdateParameter: (data: { + promptName?: string; + parameterName: string; + oldParameterName?: string; + parameterValue?: string; + }) => void; + initialItemValue?: Parameter; + removeParameter: (parameterName?: string) => Promise; +}) { + const { initialItemValue, removeParameter, onUpdateParameter } = props; + // TODO: saqadri - update this once we have a readonly mode + const { isReadonly } = { isReadonly: false }; + + const [parameterName, setParameterName] = useState( + initialItemValue?.parameterName ?? "" + ); + const [lastParameterName, setLastParameterName] = + useState(parameterName); + + const parameterValue = initialItemValue?.parameterValue; + + const [parameterValueString, setParameterValueString] = useState( + typeof parameterValue === "string" + ? parameterValue + : JSON.stringify(parameterValue) + ); + + const debouncedCellParameterUpdate = useMemo( + () => + debounce((newParameterName: string, newParameterValue: string) => { + if (!isValidParameterName(newParameterName)) { + return; + } + + onUpdateParameter({ + oldParameterName: lastParameterName, + parameterName: newParameterName, + parameterValue: newParameterValue, + }); + + setLastParameterName(newParameterName); + }, 250), + [lastParameterName, onUpdateParameter] + ); + + const theme = useMantineTheme(); + const border = + theme.colorScheme === "dark" ? "1px solid #2C2E33" : "1px solid #e9ecef"; + + return ( + + + { + setParameterName(event.target.value); + if (event.target.value) { + debouncedCellParameterUpdate( + event.target.value, + parameterValueString + ); + } + }} + /> +