diff --git a/apps/playground/package.json b/apps/playground/package.json index 22cf4e5b..0e687fb0 100644 --- a/apps/playground/package.json +++ b/apps/playground/package.json @@ -36,9 +36,11 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^4.8.0", + "react-toastify": "^10.0.4", "semantic-ui-css": "^2.5.0", "semantic-ui-react": "^2.1.4", "tar": "^6.1.13", + "uuidv4": "^6.2.13", "vite-plugin-externals": "^0.6.2", "vite-plugin-node-polyfills": "^0.7.0", "vscode-json-languageservice": "^5.3.2", diff --git a/apps/playground/src/Editor.tsx b/apps/playground/src/Editor.tsx index 286ce5ca..d8eee523 100644 --- a/apps/playground/src/Editor.tsx +++ b/apps/playground/src/Editor.tsx @@ -62,7 +62,8 @@ import { Header } from "./Header"; import { useTimeout } from "usehooks-ts"; import { TooSlowAlert } from "@wing-playground/shared/src/alerts/TooSlow"; import { ServerErrorAlert } from "@wing-playground/shared/src/alerts/ServerError"; -import {ServerDown} from "@wing-playground/shared/src/alerts/ServerDown"; +import { ServerDown } from "@wing-playground/shared/src/alerts/ServerDown"; +import { AiInput } from "./ai"; const wingPackageJson = await import( "@winglang/compiler/package.json?raw" @@ -222,6 +223,10 @@ export const ReactMonacoEditor: React.FC = ({}) => { } }, [languageContext]); + const setEditorValue = (code: string) => { + editorRef.current?.setValue(code); + }; + const simulatorTarget: TargetView = useMemo(() => { return { id: "simulator", @@ -264,7 +269,7 @@ export const ReactMonacoEditor: React.FC = ({}) => { const getAlert = () => { if (IS_SERVER_DOWN) { - return ServerDown; + return ServerDown; } if (tooSlow) { return TooSlowAlert; @@ -340,7 +345,10 @@ export const ReactMonacoEditor: React.FC = ({}) => { - + ) => void; + placeholder: string; +}) => ( + +); + +export const Button = (props: { + onClick: (e: React.MouseEvent) => void; + isLoading?: boolean; + children: any; +}) => ( + +); diff --git a/apps/playground/src/ai/icons.tsx b/apps/playground/src/ai/icons.tsx new file mode 100644 index 00000000..9facb564 --- /dev/null +++ b/apps/playground/src/ai/icons.tsx @@ -0,0 +1,33 @@ +export const AiIcon = ({ className = "" }: { className?: string }) => ( + + + +); + +export const FixIcon = ({ className = "" }: { className?: string }) => ( + + + +); diff --git a/apps/playground/src/ai/index.tsx b/apps/playground/src/ai/index.tsx new file mode 100644 index 00000000..c6dbd772 --- /dev/null +++ b/apps/playground/src/ai/index.tsx @@ -0,0 +1,138 @@ +import { uuid } from "uuidv4"; +import { useEffect, useState } from "react"; +import { AiIcon, FixIcon } from "./icons"; +import { Button, Input } from "./ctas"; +import { ToastContainer, toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; + +const formatCode = (code: string) => { + let isCode = false; + return code.split("\n").reduce((acc, line) => { + if (line.includes("```")) { + isCode = !isCode; + + const newLine = line.replace(/```wing/g, "").replace(/```/g, ""); + return acc ? [acc, newLine].join("\n") : newLine; + } + const nextLine = isCode ? line : `// ${line}`; + return acc ? [acc, nextLine].join("\n") : nextLine; + }, ""); +}; + +const AI_API_URL = + "https://bxsblg1sf2.execute-api.us-east-1.amazonaws.com/prod"; + +const LOADING_STATES = { + GENERATE: "GENERATE", + FIX: "FIX", + NONE: "", +}; + +export const AiInput = ({ + onAiAnswer, + code = "", +}: { + onAiAnswer: (prompt: string) => void; + code?: string; +}) => { + const [prompt, setPrompt] = useState(""); + const [isLoading, setIsLoading] = useState(LOADING_STATES.NONE); + + useEffect(() => { + if (sessionStorage.getItem("conversation-id") === null) { + sessionStorage.setItem("conversation-id", uuid()); + } + }, []); + + const askAi = async () => { + setIsLoading(LOADING_STATES.GENERATE); + try { + const res = await fetch(`${AI_API_URL}/generate`, { + method: "POST", + body: JSON.stringify({ prompt, code }), + headers: { + "conversation-id": sessionStorage.getItem("conversation-id")!, + "Access-Control-Allow-Origin": "http://localhost:5173", + "Access-Control-Allow-Credentials": "true", + }, + }); + if (res.ok) { + onAiAnswer(formatCode(await res.text())); + } + setPrompt(""); + } catch (error) { + toast.error((error as Error).message); + } finally { + setIsLoading(LOADING_STATES.NONE); + } + }; + + const fixCode = async () => { + try { + setIsLoading(LOADING_STATES.FIX); + const res = await fetch(`${AI_API_URL}/fix`, { + method: "POST", + body: JSON.stringify({ code }), + headers: { + "conversation-id": sessionStorage.getItem("conversation-id")!, + "Access-Control-Allow-Origin": "http://localhost:5173", + "Access-Control-Allow-Credentials": "true", + }, + }); + if (res.ok) { + onAiAnswer(formatCode(await res.text())); + } + } catch (error) { + toast.error((error as Error).message); + } finally { + setIsLoading(LOADING_STATES.NONE); + } + }; + + const restart = () => { + sessionStorage.setItem("conversation-id", uuid()); + + setPrompt(""); + onAiAnswer( + "// Write something down to get started, or use the examples above...", + ); + }; + + return ( +
+
+

+ Use wingAi +

+ +
+ +
+
+ +
+ + ) => + setPrompt(e.target.value) + } + placeholder="Or ask me to build something new..." + /> + + + +
+
+ ); +}; diff --git a/apps/playground/tsconfig.json b/apps/playground/tsconfig.json index 3d0a51a8..7b0e2287 100644 --- a/apps/playground/tsconfig.json +++ b/apps/playground/tsconfig.json @@ -16,6 +16,6 @@ "noEmit": true, "jsx": "react-jsx" }, - "include": ["src"], + "include": ["src/**/*.tsx"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/package-lock.json b/package-lock.json index 8a4f8234..a891b1cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,9 +84,11 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^4.8.0", + "react-toastify": "^10.0.4", "semantic-ui-css": "^2.5.0", "semantic-ui-react": "^2.1.4", "tar": "^6.1.13", + "uuidv4": "^6.2.13", "vite-plugin-externals": "^0.6.2", "vite-plugin-node-polyfills": "^0.7.0", "vscode-json-languageservice": "^5.3.2", @@ -6624,6 +6626,11 @@ "integrity": "sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==", "dev": true }, + "node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" + }, "node_modules/@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -26614,6 +26621,26 @@ "react-dom": ">=16.8" } }, + "node_modules/react-toastify": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.4.tgz", + "integrity": "sha512-etR3RgueY8pe88SA67wLm8rJmL1h+CLqUGHuAoNsseW35oTGJEri6eBTyaXnFKNQ80v/eO10hBYLgz036XRGgA==", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-toastify/node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -29098,6 +29125,15 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uuidv4": { + "version": "6.2.13", + "resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.13.tgz", + "integrity": "sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ==", + "dependencies": { + "@types/uuid": "8.3.4", + "uuid": "8.3.2" + } + }, "node_modules/uvu": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",