From e0a7d15d1cdd9b2a98695f2f4a92a9f12da3023b Mon Sep 17 00:00:00 2001 From: yousefed Date: Fri, 6 Oct 2023 10:31:06 +0200 Subject: [PATCH 1/6] ai wip --- package-lock.json | 515 +++++++++++++++++- packages/frame/package.json | 2 + packages/frame/src/Frame.tsx | 44 +- packages/frame/src/ai/ai.ts | 383 +++++++++++++ packages/frame/src/ai/applyChanges.ts | 195 +++++++ packages/frame/src/ai/types.ts | 90 +++ packages/frame/src/ai/yjsDiff.test.ts | 60 ++ packages/frame/src/ai/yjsDiff.ts | 75 +++ .../src/runtime/editor/compilerOptions.ts | 1 + .../editor/prettier/diffToMonacoTextEdits.ts | 34 +- .../src/runtime/editor/prettier/trimPatch.ts | 30 + packages/frame/src/stringify.ts | 132 +++++ 12 files changed, 1501 insertions(+), 60 deletions(-) create mode 100644 packages/frame/src/ai/ai.ts create mode 100644 packages/frame/src/ai/applyChanges.ts create mode 100644 packages/frame/src/ai/types.ts create mode 100644 packages/frame/src/ai/yjsDiff.test.ts create mode 100644 packages/frame/src/ai/yjsDiff.ts create mode 100644 packages/frame/src/runtime/editor/prettier/trimPatch.ts create mode 100644 packages/frame/src/stringify.ts diff --git a/package-lock.json b/package-lock.json index 8055bff2c..2f8da2e3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -4692,6 +4691,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.1.tgz", "integrity": "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==" }, + "node_modules/@types/node-fetch": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-95X8guJYhfqiuVVhRFxVQcf4hW/2bCuoPwDasMf/531STFoNoWTT7YDnWdXHEZKqAGUigmpG31r2FE70LwnzJw==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/object.omit": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/object.omit/-/object.omit-3.0.0.tgz", @@ -5306,6 +5314,118 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vue/compiler-core": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz", + "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==", + "peer": true, + "dependencies": { + "@babel/parser": "^7.21.3", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz", + "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==", + "peer": true, + "dependencies": { + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz", + "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==", + "peer": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-ssr": "3.3.4", + "@vue/reactivity-transform": "3.3.4", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.0", + "postcss": "^8.1.10", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz", + "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz", + "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==", + "peer": true, + "dependencies": { + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/reactivity-transform": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz", + "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==", + "peer": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.0" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz", + "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==", + "peer": true, + "dependencies": { + "@vue/reactivity": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz", + "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==", + "peer": true, + "dependencies": { + "@vue/runtime-core": "3.3.4", + "@vue/shared": "3.3.4", + "csstype": "^3.1.1" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz", + "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==", + "peer": true, + "dependencies": { + "@vue/compiler-ssr": "3.3.4", + "@vue/shared": "3.3.4" + }, + "peerDependencies": { + "vue": "3.3.4" + } + }, + "node_modules/@vue/shared": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", + "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==", + "peer": true + }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -5318,11 +5438,21 @@ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "dev": true }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -5372,6 +5502,17 @@ "node": ">= 6.0.0" } }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/aggregate-error": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-4.0.1.tgz", @@ -5388,6 +5529,60 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ai": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/ai/-/ai-2.2.14.tgz", + "integrity": "sha512-4kL2iYPVhH1pl6jJFIJCYcgx5mHzGOmdwiSYWVadmSkNOxKqokgevHyJKiyL9B9DjlreM9cDqkQop56Hdfkb0w==", + "dependencies": { + "eventsource-parser": "1.0.0", + "nanoid": "3.3.6", + "solid-swr-store": "0.10.7", + "sswr": "2.0.0", + "swr": "2.2.0", + "swr-store": "0.10.6", + "swrv": "1.0.4" + }, + "engines": { + "node": ">=14.6" + }, + "peerDependencies": { + "react": "^18.2.0", + "solid-js": "^1.7.7", + "svelte": "^3.0.0 || ^4.0.0", + "vue": "^3.3.4" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/ai/node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -5466,7 +5661,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, "dependencies": { "dequal": "^2.0.3" } @@ -5659,8 +5853,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -5695,7 +5888,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", - "dev": true, "dependencies": { "dequal": "^2.0.3" } @@ -5798,6 +5990,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -6315,6 +6512,28 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/code-red": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", + "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.1", + "acorn": "^8.10.0", + "estree-walker": "^3.0.3", + "periscopic": "^3.1.0" + } + }, + "node_modules/code-red/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -6332,7 +6551,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -6551,6 +6769,19 @@ "jss-preset-default": "^10.10.0" } }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "peer": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/css-vendor": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", @@ -6723,7 +6954,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -6758,6 +6988,15 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/digest-fetch": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", + "integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==", + "dependencies": { + "base-64": "^0.1.0", + "md5": "^2.3.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -7593,8 +7832,7 @@ "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, "node_modules/esutils": { "version": "2.0.3", @@ -7605,6 +7843,14 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -7614,6 +7860,14 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource-parser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.0.0.tgz", + "integrity": "sha512-9jgfSCa3dmEme2ES3mPByGXfgZ87VbP97tng1G2nWwWx6bV2nYxm2AWCrbQjXToSe+yYlqaZNtxffR9IeQr95g==", + "engines": { + "node": ">=14.18" + } + }, "node_modules/execa": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", @@ -7910,7 +8164,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -7920,6 +8173,31 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "engines": { + "node": ">= 14" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -8603,6 +8881,14 @@ "node": ">=14.18.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/hyphenate-style-name": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", @@ -9040,6 +9326,15 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true }, + "node_modules/is-reference": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "peer": true, + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -9744,6 +10039,12 @@ "lie": "3.1.1" } }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "peer": true + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -9858,7 +10159,6 @@ "version": "0.30.3", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.3.tgz", "integrity": "sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==", - "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, @@ -10164,6 +10464,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "peer": true + }, "node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", @@ -10753,7 +11059,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -10762,7 +11067,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -11247,7 +11551,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "dev": true, "funding": [ { "type": "github", @@ -11559,6 +11862,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.11.1.tgz", + "integrity": "sha512-GU0HQWbejXuVAQlDjxIE8pohqnjptFDIm32aPlNT1H9ucMz1VJJD0DaTJRQsagNaJ97awWjjVLEG7zCM6sm4SA==", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "digest-fetch": "^1.3.0", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.18.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.3.tgz", + "integrity": "sha512-0OVfGupTl3NBFr8+iXpfZ8NR7jfFO+P1Q+IO/q0wbo02wYkP5gy36phojeYWpLQ6WAMjl+VfmqUk2YbUfp0irA==" + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -12049,11 +12375,30 @@ "resolved": "https://registry.npmjs.org/penpal/-/penpal-6.2.2.tgz", "integrity": "sha512-RQD7hTx14/LY7QoS3tQYO3/fzVtwvZI+JeS5udgsu7FPaEDjlvfK9HBcme9/ipzSPKnrxSgacI9PI7154W62YQ==" }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, + "node_modules/periscopic/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -12217,7 +12562,6 @@ "version": "8.4.28", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz", "integrity": "sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==", - "dev": true, "funding": [ { "type": "opencollective", @@ -12245,7 +12589,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true, "funding": [ { "type": "github", @@ -13548,6 +13891,15 @@ "randombytes": "^2.1.0" } }, + "node_modules/seroval": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-0.5.1.tgz", + "integrity": "sha512-ZfhQVB59hmIauJG5Ydynupy8KHyr5imGNtdDhbZG68Ufh1Ynkv9KOYOAABf71oVbQxJ8VkWnMHAjEHE7fWkH5g==", + "peer": true, + "engines": { + "node": ">=10" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -13633,6 +13985,28 @@ "node": ">=6" } }, + "node_modules/solid-js": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.7.12.tgz", + "integrity": "sha512-QoyoOUKu14iLoGxjxWFIU8+/1kLT4edQ7mZESFPonsEXZ//VJtPKD8Ud1aTKzotj+MNWmSs9YzK6TdY+fO9Eww==", + "peer": true, + "dependencies": { + "csstype": "^3.1.0", + "seroval": "^0.5.0" + } + }, + "node_modules/solid-swr-store": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/solid-swr-store/-/solid-swr-store-0.10.7.tgz", + "integrity": "sha512-A6d68aJmRP471aWqKKPE2tpgOiR5fH4qXQNfKIec+Vap+MGQm3tvXlT8n0I8UgJSlNAsSAUuw2VTviH2h3Vv5g==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "solid-js": "^1.2", + "swr-store": "^0.10" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -13645,7 +14019,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -13686,6 +14059,17 @@ "node": ">=0.10.0" } }, + "node_modules/sswr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sswr/-/sswr-2.0.0.tgz", + "integrity": "sha512-mV0kkeBHcjcb0M5NqKtKVg/uTIYNlIIniyDfSGrSfxpEdM9C365jK0z55pl9K0xAkNTJi2OAOVFQpgMPUk+V0w==", + "dependencies": { + "swrev": "^4.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -13926,6 +14310,74 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svelte": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.1.tgz", + "integrity": "sha512-LpLqY2Jr7cRxkrTc796/AaaoMLF/1ax7cto8Ot76wrvKQhrPmZ0JgajiWPmg9mTSDqO16SSLiD17r9MsvAPTmw==", + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/trace-mapping": "^0.3.18", + "acorn": "^8.9.0", + "aria-query": "^5.3.0", + "axobject-query": "^3.2.1", + "code-red": "^1.0.3", + "css-tree": "^2.3.1", + "estree-walker": "^3.0.3", + "is-reference": "^3.0.1", + "locate-character": "^3.0.0", + "magic-string": "^0.30.0", + "periscopic": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/svelte/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/swr": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz", + "integrity": "sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==", + "dependencies": { + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/swr-store": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/swr-store/-/swr-store-0.10.6.tgz", + "integrity": "sha512-xPjB1hARSiRaNNlUQvWSVrG5SirCjk2TmaUyzzvk69SZQan9hCJqw/5rG9iL7xElHU784GxRPISClq4488/XVw==", + "dependencies": { + "dequal": "^2.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/swrev/-/swrev-4.0.0.tgz", + "integrity": "sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA==" + }, + "node_modules/swrv": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/swrv/-/swrv-1.0.4.tgz", + "integrity": "sha512-zjEkcP8Ywmj+xOJW3lIT65ciY/4AL4e/Or7Gj0MzU3zBJNMdJiT8geVZhINavnlHRMMCcJLHhraLTAiDOTmQ9g==", + "peerDependencies": { + "vue": ">=3.2.26 < 4" + } + }, "node_modules/symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", @@ -14756,6 +15208,14 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/utf-8-validate": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", @@ -15049,6 +15509,19 @@ "resolved": "https://registry.npmjs.org/vscode-lib/-/vscode-lib-0.1.2.tgz", "integrity": "sha512-X7YTInfdx0D7O5d5jxv5tirYNlZT3wwmB/auEWDq8nKrJCkZea48y1brADKWSfmmSCvmaZwG5RJ3VOQf/pPwMg==" }, + "node_modules/vue": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", + "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-sfc": "3.3.4", + "@vue/runtime-dom": "3.3.4", + "@vue/server-renderer": "3.3.4", + "@vue/shared": "3.3.4" + } + }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", @@ -16029,12 +16502,14 @@ "@typecell-org/shared": "^0.0.3", "@typecell-org/util": "^0.0.3", "@typecell-org/y-penpal": "^0.0.3", + "ai": "2.2.14", "localforage": "^1.10.0", "lz-string": "^1.4.4", "mobx": "^6.2.0", "mobx-react-lite": "^3.2.0", "mobx-utils": "^6.0.8", "monaco-editor": "^0.35.0", + "openai": "^4.11.1", "penpal": "^6.1.0", "prettier": "^3.0.2", "prosemirror-keymap": "^1.2.2", diff --git a/packages/frame/package.json b/packages/frame/package.json index b88619bce..384443b94 100644 --- a/packages/frame/package.json +++ b/packages/frame/package.json @@ -19,6 +19,8 @@ "mobx": "^6.2.0", "mobx-react-lite": "^3.2.0", "mobx-utils": "^6.0.8", + "openai": "^4.11.1", + "ai": "2.2.14", "prosemirror-model": "^1.19.3", "prosemirror-view": "^1.31.7", "prosemirror-state": "^1.4.3", diff --git a/packages/frame/src/Frame.tsx b/packages/frame/src/Frame.tsx index 1ec26f1ad..165bd0995 100644 --- a/packages/frame/src/Frame.tsx +++ b/packages/frame/src/Frame.tsx @@ -32,17 +32,19 @@ import styles from "./Frame.module.css"; import { MonacoBlockContent } from "./MonacoBlockContent"; import { RichTextContext } from "./RichTextContext"; import SourceModelCompiler from "./runtime/compiler/SourceModelCompiler"; +import { setMonacoDefaults } from "./runtime/editor"; import { MonacoContext } from "./runtime/editor/MonacoContext"; import { ExecutionHost } from "./runtime/executor/executionHosts/ExecutionHost"; import LocalExecutionHost from "./runtime/executor/executionHosts/local/LocalExecutionHost"; -import { setMonacoDefaults } from "./runtime/editor"; - import { variables } from "@typecell-org/util"; import { RiCodeSSlashFill } from "react-icons/ri"; +import { VscWand } from "react-icons/vsc"; import { EditorStore } from "./EditorStore"; import { MonacoColorManager } from "./MonacoColorManager"; import monacoStyles from "./MonacoSelection.module.css"; +import { getAICode } from "./ai/ai"; +import { applyChanges } from "./ai/applyChanges"; import { setupTypecellHelperTypeResolver } from "./runtime/editor/languages/typescript/TypeCellHelperTypeResolver"; import { setupTypecellModuleTypeResolver } from "./runtime/editor/languages/typescript/TypeCellModuleTypeResolver"; import { setupNpmTypeResolver } from "./runtime/editor/languages/typescript/npmTypeResolver"; @@ -248,6 +250,7 @@ export const Frame: React.FC = observer((props) => { monaco, newEngine, ); + return [ { newCompiler, newExecutionHost }, () => { @@ -262,6 +265,35 @@ export const Frame: React.FC = observer((props) => { slashMenuItems.splice( originalItems.length, slashMenuItems.length, + { + name: "AI", + execute: async (editor: BlockNoteEditor) => { + const p = prompt("AI"); + + const commands = await getAICode(p!, tools.newExecutionHost, editor); + // debugger; + // const commands = [ + // { + // // afterId: "3d70d0b1-02d7-4103-b145-452fafb93884", + // afterId: editor.topLevelBlocks[1].id, + // type: "add", + // content: + // "// This is a code block\nexport let value = 10;\nconsole.log(value);", + // blockType: "codeblock", + // } as const, + // ]; + applyChanges( + commands, + document.ydoc.getXmlFragment("doc"), + document.awareness, + ); + // console.log(response); + }, + aliases: ["ai", "magic"], + hint: "Prompt your AI code assistant", + group: "Code", + icon: , + }, ...[...editorStore.current.customBlocks.values()].map((data: any) => { console.log("update blocks"); return { @@ -293,17 +325,13 @@ export const Frame: React.FC = observer((props) => { type: "codeblock", props: { language: "typescript", - // moduleName: moduleName, - // key, storage: "", }, content: `// @default-collapsed import * as doc from "${data.documentId}"; export let ${varName} = doc.${data.blockVariable}; -// export default { -// block: doc.${data.blockVariable}, -// doc, -// }; +export let ${varName}Scope = doc; +export default ${varName}; `, } as any, ); diff --git a/packages/frame/src/ai/ai.ts b/packages/frame/src/ai/ai.ts new file mode 100644 index 000000000..9b24917b4 --- /dev/null +++ b/packages/frame/src/ai/ai.ts @@ -0,0 +1,383 @@ +// import LocalExecutionHost from "../../../runtime/executor/executionHosts/local/LocalExecutionHost" +import "@blocknote/core/style.css"; +import { OpenAIStream, StreamingTextResponse } from "ai"; +import * as mobx from "mobx"; +import * as monaco from "monaco-editor"; +import { OpenAI } from "openai"; + +import { BlockNoteEditor } from "@blocknote/core"; +import { uri } from "vscode-lib"; +import { compile } from "../runtime/compiler/compilers/MonacoCompiler"; +import { ExecutionHost } from "../runtime/executor/executionHosts/ExecutionHost"; +import { customStringify } from "../stringify"; +import { CodeBlockRuntimeInfo } from "./types"; + +// and for the runtime info: + +// /** +// * Runtime information about a code block of the main document +// * The code itself is not included (it's in the Block.id with the corresponding blockId) +// */ +// type MainCodeBlockRuntimeInfo = { +// imported: false; +// blockId: string; +// // .d.ts TypeScript types of values exported by this block +// types: string; +// // the runtime values exported by this block. Data can be trimmed for brevity +// // IMPORTANT: if this is for example { outputVariable: 5 }, it means the code block exports a variable \`outputVariable\` and the current value is 5 +// // You can access this reactive variable in other code blocks using the \`$\` variable. e.g.: \`$.outputVariable\` (it supports both reading and writing) +// data: any; +// }; + +// /** +// * Runtime + code information of code blocks imported from other documents +// */ +// type ImportedCodeBlockRuntimeInfo = { +// imported: true; +// /** +// * Because we don't pass the entire document this code is imported from, we need to pass the code of this code block +// */ +// code: string; +// // .d.ts TypeScript types of values exported by this block +// types: string; +// documentId: string; +// blockId: string; +// // the runtime values exported by this block. Data can be trimmed for brevity +// data: any; +// }; + +// export type CodeBlockRuntimeInfo = +// | MainCodeBlockRuntimeInfo +// | ImportedCodeBlockRuntimeInfo; + +const TYPECELL_PROMPT = ` +You're a smart AI assistant for TypeCell: a rich text document tool that also supports interactive Code Blocks written in Typescript. + +TypeCell Documents look like this: +- Documents consists of a list of blocks (e.g.: headings, paragraphs, code blocks), Notion style. Code Blocks are unique to TypeCell and execute live, as-you type. + +TypeCell Code Blocks works as follows: +- Code Blocks can export variables using the javascript / typescript \`export\` syntax. These variables are shown as the output of the cell. +- The exported variables by a Code Block are available in other cells, under the \`$\` variable. e.g.: \`$.exportedVariableFromOtherCell\` +- Different cells MUST NOT output variables with the same name, because then they would collide under the \`$\` variable. +- When the exports of one Code Block change, other cells that depend on those exports, update live, automatically. +- React / JSX components will be displayed automatically. E.g.: \`export let component =
hello world
\` will display a div with hello world. +- Note that exported functions are not called automatically. They'll simply become a callable variable under the $ scope. This means simply exporting a function and not calling it anywhere is not helpful + +Example document: + +[ + { + id: "block-1", + type: "codeblock", + content: "export let name = 'James';", + }, + { + id: "block-2", + type: "codeblock", + content: "export let nameLength = $.name.length; // updates reactively based on the $.name export from block-1", + }, + { + id: "block-3", + type: "codeblock", + content: "// This uses the exported \`name\` from code block 1, using the TypeCell \`$.name\` syntax, and shows the capitalized name using React + export let capitalized =
{$.name.toUpperCase()}
", + } +] + +The runtime data of this would be: + +{ name: "James", nameLength: 5, capitalized: "[REACTELEMENT]"} + +This is the type of a document: + +type Block = { + id: string; + type: "paragraph" | "heading" | "codeblock"; + content?: string; + children?: Block[]; +}; + +export type Document = Block[]; + +Example prompts: +- If the user would ask you to update the name in the document, you would issue an Update operation to block-1. +- If the user would ask you to add a button to prompt for a name, you would issue an Add operation for a new codeblock with code \`export default \` +- If the user would ask you to output the name in reverse, you would issue an Add operation with code \`export let reverseName = $.name.split('').reverse().join('');\` + +NEVER write code that depends on and updates the same variable, as that would cause a loop. You can directly modify (mutate) variables. So don't do this: + +$.complexObject = { ...$.complexObject, newProperty: 5 }; + +but instead: + +$.complexObject.newProperty = 5; +`; + +const openai = new OpenAI({ + apiKey: "", + dangerouslyAllowBrowser: true, +}); + +export async function getAICode( + prompt: string, + executionHost: ExecutionHost, + editor: BlockNoteEditor, +) { + const models = monaco.editor.getModels(); + const typeCellModels = models.filter((m) => + m.uri.path.startsWith("/!typecell:typecell.org"), + ); + const blocks = editor.topLevelBlocks; + + const tmpModel = monaco.editor.createModel( + "", + "typescript", + uri.URI.parse("file:///tmp.tsx"), + ); + tmpModel.setValue(`import React from "!typecell:typecell.org/dqBLFEyFuSUu1"; + import * as $ from "!typecell:typecell.org/dqBLFEyFuSUu1"; + // expands object types one level deep +type Expand = T extends infer O ? { [K in keyof O]: O[K] extends { Key: React.Key | null } ? "[REACT]" : O[K] } : never; + +// expands object types recursively +type ExpandRecursively = T extends object + ? T extends infer O ? { [K in keyof O]: O[K] extends { key: React.Key } ? "[REACT ELEMENT]" : ExpandRecursively } : never + : T; + + // ? T extends infer O ? { [K in keyof O]: ExpandRecursively } : never + type ContextType = ExpandRecursively;`); + + const worker = await monaco.languages.typescript.getTypeScriptWorker(); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const ts = (await worker(tmpModel.uri))!; + const pos = + tmpModel.getValue().length - "pe = ExpandRecursively;".length; + // const def = await ts.getDefinitionAtPosition(tmpModel.uri.toString(), pos); + const def2 = await ts.getQuickInfoAtPosition(tmpModel.uri.toString(), pos); + const contextType = def2.displayParts.map((x: any) => x.text).join(""); + // const def3 = await ts.get(tmpModel.uri.toString(), pos, {}); + + tmpModel.dispose(); + + const codeInfoPromises = typeCellModels.map(async (m) => { + const code = await compile(m, monaco); + const output = await executionHost.outputs.get(m.uri.toString())?.value; + + let data: any; + if (output) { + const outputJS = Object.fromEntries( + Object.getOwnPropertyNames(output).map((key) => [ + key, + mobx.toJS(output[key]), + ]), + ); + data = JSON.parse(customStringify(outputJS)); + // console.log(data); + } + const path = m.uri.path.split("/"); // /!typecell:typecell.org/dVVAYmvBaeQdE/c58863ef-2f82-4fd7-ab0c-f1f760eb9578.cell.tsx" + const blockId = path[path.length - 1].replace(".cell.tsx", ""); + const imported = !blocks.find((b) => b.id === blockId); + + const ret: CodeBlockRuntimeInfo = { + // code: imported ? m.getValue() : undefined, + types: code.types, + blockId, + data, + ...(imported + ? { documentId: path[path.length - 2]!, imported, code: m.getValue() } + : { imported }), + }; + return ret; + }); + let codeInfos = await Promise.all(codeInfoPromises); + codeInfos = codeInfos.filter((x) => !!x.imported); + + const context = executionHost.engine.observableContext.rawContext as any; + + let outputJS = Object.fromEntries( + Object.getOwnPropertyNames(context).map((key) => [ + key, + mobx.toJS(context[key]), + ]), + ); + outputJS = JSON.parse(customStringify(outputJS)); + + function cleanBlock(block: any) { + if (!block.content?.length && !block.children?.length) { + return undefined; + } + delete block.props; + if (block.children) { + block.children = block.children.map(cleanBlock); + } + block.content = block.content.map((x: any) => x.text).join(""); + return block; + } + // console.log("request", JSON.stringify(blocks).length); + const sanitized = blocks.map(cleanBlock).filter((x) => !!x); + // console.log("sanitized", JSON.stringify(sanitized).length); + + // debugger; + // const command = prompt("prompt"); + const contextInfo = + contextType.replace("type ContextType = ", "const $: ") + + " = " + + JSON.stringify(outputJS); + // Ask OpenAI for a streaming chat completion given the prompt + const response = await openai.chat.completions.create({ + // model: "gpt-3.5-turbo-16k", + model: "gpt-4", + stream: true, + messages: [ + { + role: "system", + content: TYPECELL_PROMPT, + }, + { + role: "user", + content: `This is my document data: +"""${JSON.stringify(sanitized)}"""`, + }, + { + role: "user", + content: + "This is the type and runtime data available under the reactive $ variable for read / write access. If you need to change / read some information from the live document, it's likely you need to access it from here using $. \n" + + contextInfo, + }, + // codeInfos.length + // ? { + // role: "user", + // content: `This is the runtime / compiler data of the Code Blocks (CodeBlockRuntimeInfo[]): + // """${JSON.stringify(codeInfos)}"""`, + // } + // : { + // role: "user", + // content: `There are no code blocks in the document, so there's no runtime / compiler data for these (CodeBlockRuntimeInfo[]).`, + // }, + { + role: "system", + content: `You are an AI assistant helping user to modify his document. This means changes can either be code related (in that case, you'll need to add / modify Code Blocks), + or not at all (in which case you'll need to add / modify regular blocks), or a mix of both.`, + }, + { + role: "user", + content: prompt, // + + // " . \n\nRemember to reply ONLY with OperationsResponse JSON (DO NOT add any further comments). So start with [{ and end with }]", + }, + ], + functions: [ + { + name: "updateDocument", + description: "Update the document with operations", + parameters: { + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + required: ["operations"], + properties: { + operations: { + type: "array", + items: { + oneOf: [ + { + type: "object", + properties: { + explanation: { + type: "string", + description: + "explanation of why this block was deleted (your reasoning as AI agent)", + }, + type: { + type: "string", + enum: ["delete"], + description: + "Operation to delete a block in the document", + }, + id: { + type: "string", + description: "id of block to delete", + }, + }, + required: ["type", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + explanation: { + type: "string", + description: + "explanation of why this block was updated (your reasoning as AI agent)", + }, + type: { + type: "string", + enum: ["update"], + description: + "Operation to update a block in the document", + }, + id: { + type: "string", + description: "id of block to delete", + }, + content: { + type: "string", + description: "new content of block", + }, + }, + required: ["type", "id", "content"], + additionalProperties: false, + }, + { + type: "object", + properties: { + explanation: { + type: "string", + description: + "explanation of why this block was added (your reasoning as AI agent)", + }, + type: { + type: "string", + enum: ["add"], + description: + "Operation to insert a new block in the document", + }, + afterId: { + type: "string", + description: + "id of block after which to insert a new block in the document", + }, + content: { + type: "string", + description: "content of new block", + }, + blockType: { + type: "string", + enum: ["codeblock", "paragraph", "heading"], + description: "type of new block", + }, + }, + required: ["afterId", "type", "content", "blockType"], + additionalProperties: false, + }, + ], + }, + }, + }, + }, + }, + ], + function_call: { + name: "updateDocument", + }, + }); + + const stream = OpenAIStream(response); + + // Respond with the stream + const ret = new StreamingTextResponse(stream); + const data = await ret.json(); + console.log(data); + + return JSON.parse(data.function_call.arguments).operations; +} diff --git a/packages/frame/src/ai/applyChanges.ts b/packages/frame/src/ai/applyChanges.ts new file mode 100644 index 000000000..79ab87ec6 --- /dev/null +++ b/packages/frame/src/ai/applyChanges.ts @@ -0,0 +1,195 @@ +import { error, uniqueId } from "@typecell-org/util"; +import { Awareness } from "y-protocols/awareness"; +import * as Y from "yjs"; +import { BlockOperation, OperationsResponse } from "./types"; +import { getYjsDiffs } from "./yjsDiff"; + +function findBlock(id: string, data: Y.XmlFragment) { + const node = data + .createTreeWalker( + (el) => el instanceof Y.XmlElement && el.getAttribute("id") === id, + ) + .next(); + if (node.done) { + return undefined; + } + return node.value as Y.XmlElement; +} + +function findParentIndex(node: Y.XmlFragment) { + const parent = node.parent as Y.XmlElement; + for (let i = 0; i < parent.length; i++) { + if (parent.get(i) === node) { + return i; + } + } + throw new Error("not found"); +} + +function updateState( + awareness: Awareness, + head: Y.RelativePosition, + anchor: Y.RelativePosition, +) { + // const initial = !awareness.states.has(99); + awareness.states.set(99, { + user: { + name: "@AI", + color: "#94FADB", + }, + cursor: { + anchor, + head, + // "anchor": { + // "type": { + // "client": 1521604366, + // "clock": 5 + // }, + // "tname": null, + // "item": { + // "client": 1521604366, + // "clock": 22 + // }, + // "assoc": 0 + // }, + // "head": { + // "type": { + // "client": 1521604366, + // "clock": 5 + // }, + // "tname": null, + // "item": { + // "client": 1521604366, + // "clock": 41 + // }, + // "assoc": 0 + // } + }, + }); + + // if (!initial) { + // awareness.emit("update", [ + // { + // added: [99], + // updated: [], + // removed: [], + // }, + // origin, + // ]); + // } + awareness.emit("change", [ + { + added: 0, + updated: 1, + removed: 0, + }, + origin, + ]); +} + +export async function applyChange( + op: BlockOperation, + data: Y.XmlFragment, + awareness: Awareness, +) { + if (op.type === "add") { + const node = findBlock(op.afterId, data); + if (!node) { + throw new Error("Block not found"); + } + const newElement = new Y.XmlElement("blockContainer"); + const child = new Y.XmlElement(op.blockType); + child.setAttribute("id", uniqueId.generateId("block")); + const yText = new Y.XmlText(); + child.insert(0, [yText]); + newElement.insert(0, [child]); + // TODO: create block + (node.parent as Y.XmlElement).insertAfter(node, [newElement]); + + // start typing text content + for (let i = 0; i < op.content.length; i++) { + const start = Y.createRelativePositionFromTypeIndex(yText, i); + const end = Y.createRelativePositionFromTypeIndex(yText, i); + updateState(awareness, start, end); + // return new RelativeSelection(start, end, sel.getDirection()) + + yText.insert(i, op.content[i]); + await new Promise((resolve) => setTimeout(resolve, 20)); + } + } else if (op.type === "delete") { + const node = findBlock(op.id, data); + if (!node) { + throw new Error("Block not found"); + } + const blockNode = node.firstChild as Y.XmlElement; + const yText = blockNode.firstChild as Y.XmlText; + + const start = Y.createRelativePositionFromTypeIndex(yText, 0); + const end = Y.createRelativePositionFromTypeIndex(yText, yText.length - 1); + + updateState(awareness, start, end); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + (node.parent as Y.XmlElement).delete(findParentIndex(node), 1); + await new Promise((resolve) => setTimeout(resolve, 20)); + } else if (op.type === "update") { + const node = findBlock(op.id, data); + if (!node) { + throw new Error("Block not found"); + } + + // let gptCode = "\n" + gptCell.code + "\n"; + // gptCode = gptCode.replaceAll("import React from 'react';\n", ""); + // gptCode = gptCode.replaceAll("import * as React from 'react';\n", ""); + // console.log("diffs", cell.code.toJSON(), gptCode); + const blockNode = node.firstChild as Y.XmlElement; + const yText = blockNode.firstChild as Y.XmlText; + const steps = getYjsDiffs(yText, op.content); + for (const step of steps) { + if (step.type === "insert") { + for (let i = 0; i < step.text.length; i++) { + const start = Y.createRelativePositionFromTypeIndex( + yText, + step.from + i, + ); + const end = Y.createRelativePositionFromTypeIndex( + yText, + step.from + i, + ); + updateState(awareness, start, end); + // return new RelativeSelection(start, end, sel.getDirection()) + + yText.insert(step.from + i, step.text[i]); + await new Promise((resolve) => setTimeout(resolve, 20)); + } + // cell.code.delete(step.from, step.length); + } else if (step.type === "delete") { + const start = Y.createRelativePositionFromTypeIndex(yText, step.from); + const end = Y.createRelativePositionFromTypeIndex( + yText, + step.from + step.length, + ); + updateState(awareness, start, end); + await new Promise((resolve) => setTimeout(resolve, 200)); + yText.delete(step.from, step.length); + await new Promise((resolve) => setTimeout(resolve, 20)); + } + } + } else { + throw new error.UnreachableCaseError(op); + } +} + +export async function applyChanges( + commands: OperationsResponse, + fragment: Y.XmlFragment, + awareness: Awareness, +) { + const doc = new Y.Doc(); + + for (const op of commands) { + await applyChange(op, fragment, awareness); + } + return doc; +} diff --git a/packages/frame/src/ai/types.ts b/packages/frame/src/ai/types.ts new file mode 100644 index 000000000..baec50832 --- /dev/null +++ b/packages/frame/src/ai/types.ts @@ -0,0 +1,90 @@ +/** + * Operation to the document + * + * Block Id `id` parameters MUST be part of the document the user is editing (NOT a block from an imported library) + */ +export type BlockOperation = + | { + type: "delete"; + id: string; + } + | { + type: "update"; + id: string; + content: string; + } + | { + afterId: string; + type: "add"; + content: string; + blockType: "codeblock" | "paragraph" | "heading"; + }; + +export type OperationsResponse = BlockOperation[]; + +export const OUTPUT_TYPES = `/** +* Operation to the document +* +* Block Id \`id\` parameters MUST be part of the document the user is editing (NOT a block from an imported library) +*/ +type BlockOperation = + | { + type: "delete"; + id: string; + } + | { + type: "update"; + id: string; + content: string; + } + | { + afterId: string; + type: "add"; + content: string; + blockType: "codeblock" | "paragraph" | "heading"; + }; + +type response = BlockOperation[];`; + +type Block = { + id: string; + type: "paragraph" | "heading" | "codeblock"; + content?: string; + children?: Block[]; +}; + +export type Document = Block[]; + +/** + * Runtime information about a code block of the main document + * The code itself is not included (it's in the Block.id with the corresponding blockId) + */ +type MainCodeBlockRuntimeInfo = { + imported: false; + blockId: string; + // .d.ts TypeScript types of values exported by this block + types: string; + // the runtime values exported by this block. Data can be trimmed for brevity + data: any; +}; + +/** + * Runtime + code information of code blocks imported from other documents + */ +type ImportedCodeBlockRuntimeInfo = { + imported: true; + /** + * Because we don't pass the entire document this code is imported from, we need to pass the code of this code block + */ + code: string; + // .d.ts TypeScript types of values exported by this block + types: string; + documentId: string; + blockId: string; + // the runtime values exported by this block. Data can be trimmed for brevity + data: any; +}; + +export type CodeBlockRuntimeInfo = + | MainCodeBlockRuntimeInfo + | ImportedCodeBlockRuntimeInfo; diff --git a/packages/frame/src/ai/yjsDiff.test.ts b/packages/frame/src/ai/yjsDiff.test.ts new file mode 100644 index 000000000..05b5ac900 --- /dev/null +++ b/packages/frame/src/ai/yjsDiff.test.ts @@ -0,0 +1,60 @@ +/** + * @vitest-environment jsdom + */ + +import { describe, expect, it } from "vitest"; +import * as Y from "yjs"; +import { getYjsDiffs } from "./yjsDiff"; + +describe("diffYjs", () => { + it("basic replace", () => { + const doc = new Y.Doc(); + + // const text = new Y.Text("hello world"); + const text = doc.getText("text"); + text.insert(0, "hello world"); + getYjsDiffs(text, "hello there world"); + expect(text.toJSON()).toEqual("hello there world"); + }); + + it("delete", () => { + const doc = new Y.Doc(); + + // const text = new Y.Text("hello world"); + const text = doc.getText("text"); + text.insert(0, "hello there world"); + getYjsDiffs(text, "hello world"); + expect(text.toJSON()).toEqual("hello world"); + }); + + it("insert and delete", () => { + const doc = new Y.Doc(); + + // const text = new Y.Text("hello world"); + const text = doc.getText("text"); + text.insert(0, "hello there world"); + getYjsDiffs(text, "hell crazy world. How are you?"); + expect(text.toJSON()).toEqual("hell crazy world. How are you?"); + }); + + it("advanced", () => { + const orig = `// This generates an array of numbers 1 through 10 + export let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];`; + + const newText = ` + + // This cell exports an array of numbers 1 through 9 + + export let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + + `; + + const doc = new Y.Doc(); + + // const text = new Y.Text("hello world"); + const text = doc.getText("text"); + text.insert(0, orig); + getYjsDiffs(text, newText); + expect(text.toJSON()).toEqual(newText); + }); +}); diff --git a/packages/frame/src/ai/yjsDiff.ts b/packages/frame/src/ai/yjsDiff.ts new file mode 100644 index 000000000..0057e8ffb --- /dev/null +++ b/packages/frame/src/ai/yjsDiff.ts @@ -0,0 +1,75 @@ +import * as Y from "yjs"; + +import diff_match_patch from "../runtime/editor/prettier/diff"; +import { trimPatch } from "../runtime/editor/prettier/trimPatch"; + +const dmp = new diff_match_patch(); + +type Step = + | { + type: "insert"; + text: string; + from: number; + } + | { + type: "delete"; + from: number; + length: number; + }; + +export function getYjsDiffs( + existing: Y.Text, + newText: string, + execute = false, +) { + const steps: Step[] = []; + + const diffs = dmp.diff_main(existing.toJSON(), newText); + const patches = dmp.patch_make(diffs); + + // let posDiff = 0; + for (const patch of patches) { + trimPatch(patch); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const startPos = patch.start1!; // + posDiff; + // posDiff += patch.length1 - patch.length2; + + let tempLengths = 0; + for (const diff of patch.diffs) { + const type = diff[0]; + const text = diff[1]; + + // type 0: keep, type 1: insert, type -1: delete + if (type === 0) { + tempLengths += text.length; + } else if (type === 1) { + // newText += text; + const actionStart = startPos + tempLengths; + steps.push({ + type: "insert", + text, + from: actionStart, + // action: () => existing.insert(actionStart, text), + }); + if (execute) { + existing.insert(actionStart, text); + } + tempLengths += text.length; + } else { + // tempLengths -= text.length; + // posDiff -= patch.length1; + const actionStart = startPos + tempLengths; + + steps.push({ + type: "delete", + from: actionStart, + length: text.length, + }); + if (execute) { + existing.delete(actionStart, text.length); + } + } + } + } + return steps; +} diff --git a/packages/frame/src/runtime/editor/compilerOptions.ts b/packages/frame/src/runtime/editor/compilerOptions.ts index 2c3cc6d95..cba375504 100644 --- a/packages/frame/src/runtime/editor/compilerOptions.ts +++ b/packages/frame/src/runtime/editor/compilerOptions.ts @@ -14,6 +14,7 @@ export function getDefaultSandboxCompilerOptions( >, ) { const settings: CompilerOptions = { + noErrorTruncation: true, noImplicitAny: true, strictNullChecks: !config.useJavaScript, strictFunctionTypes: true, diff --git a/packages/frame/src/runtime/editor/prettier/diffToMonacoTextEdits.ts b/packages/frame/src/runtime/editor/prettier/diffToMonacoTextEdits.ts index 27d7c9762..a0e61a9e8 100644 --- a/packages/frame/src/runtime/editor/prettier/diffToMonacoTextEdits.ts +++ b/packages/frame/src/runtime/editor/prettier/diffToMonacoTextEdits.ts @@ -2,40 +2,10 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import type * as monaco from "monaco-editor"; import diff_match_patch from "./diff.js"; +import { trimPatch } from "./trimPatch.js"; const dmp = new diff_match_patch(); -/** - * Trim type-0 diffs from a diff_match_patch patch. 0 indicates "keep", so is not really a diff - */ -function trimPatch(patch: any) { - // head - if (patch.diffs[0][0] === 0) { - const len = patch.diffs[0][1].length; - - // adjust patch params - patch.start1 += len; - patch.length1 -= len; - patch.start2 += len; - patch.length2 -= len; - - // remove diff - patch.diffs.shift(); - } - // tail - if (patch.diffs[patch.diffs.length - 1][0] === 0) { - const len = patch.diffs[patch.diffs.length - 1][1].length; - - // adjust patch params - patch.length1 -= len; - patch.length2 -= len; - - // remove diff - patch.diffs.pop(); - } - return patch; -} - /** * This calculates a list of Monaco TextEdit objects, that represent the transformation from * model.getValue() to v2. @@ -55,7 +25,7 @@ export function diffToMonacoTextEdits(model: monaco.editor.IModel, v2: string) { trimPatch(patch); const startPos = model.getPositionAt(patch.start1! + posDiff); const endPos = model.getPositionAt( - patch.start1! + patch.length1! + posDiff + patch.start1! + patch.length1! + posDiff, ); const range: monaco.IRange = { startColumn: startPos.column, diff --git a/packages/frame/src/runtime/editor/prettier/trimPatch.ts b/packages/frame/src/runtime/editor/prettier/trimPatch.ts new file mode 100644 index 000000000..e5d306fd6 --- /dev/null +++ b/packages/frame/src/runtime/editor/prettier/trimPatch.ts @@ -0,0 +1,30 @@ +/** + * Trim type-0 diffs from a diff_match_patch patch. 0 indicates "keep", so is not really a diff + */ +export function trimPatch(patch: any) { + // head + if (patch.diffs[0][0] === 0) { + const len = patch.diffs[0][1].length; + + // adjust patch params + patch.start1 += len; + patch.length1 -= len; + patch.start2 += len; + patch.length2 -= len; + + // remove diff + patch.diffs.shift(); + } + // tail + if (patch.diffs[patch.diffs.length - 1][0] === 0) { + const len = patch.diffs[patch.diffs.length - 1][1].length; + + // adjust patch params + patch.length1 -= len; + patch.length2 -= len; + + // remove diff + patch.diffs.pop(); + } + return patch; +} diff --git a/packages/frame/src/stringify.ts b/packages/frame/src/stringify.ts new file mode 100644 index 000000000..75c6a448c --- /dev/null +++ b/packages/frame/src/stringify.ts @@ -0,0 +1,132 @@ +import React from "react"; + +type Serializable = + | string + | number + | boolean + | null + | { [key: string]: Serializable } + | Serializable[]; + +interface QueueItem { + obj: Serializable; + path: (string | number)[]; +} + +export function customStringify(obj: Serializable, budget = 1000): string { + const seen = new Set(); + const queue: QueueItem[] = [{ obj, path: [] }]; + const output: Serializable = Array.isArray(obj) ? [] : {}; + + while (queue.length > 0 && budget > 0) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { obj: currentObj, path } = queue.shift()!; + + if (typeof currentObj !== "object" || currentObj === null) { + continue; + } + + // Handle circular references + if (seen.has(currentObj)) { + setByPath(output, path, "[CIRCULAR]"); + continue; + } + seen.add(currentObj); + + for (const key in currentObj) { + if (budget <= 0) { + break; + } + + const value = (currentObj as any)[key]; + const newPath = path.concat(key); + + if (typeof value === "string") { + if (value.length <= budget) { + setByPath(output, newPath, value); + budget -= value.length; + } else { + setByPath( + output, + newPath, + value.substring(0, budget - "[TRIMMED]".length) + "[TRIMMED]", + ); + budget = 0; + } + } else if (typeof value === "object" && value !== null) { + if (Array.isArray(value)) { + const newValue: Serializable[] = []; + setByPath(output, newPath, newValue); + if (JSON.stringify(value).length > budget) { + newValue.push("[TRIMMEDARRAY]"); + budget -= "[TRIMMEDARRAY]".length; + } else { + queue.push({ obj: value, path: newPath }); + } + } else if (React.isValidElement(value)) { + setByPath(output, newPath, "[REACTELEMENT]"); + } else { + const newValue: Serializable = {}; + setByPath(output, newPath, newValue); + if (JSON.stringify(value).length > budget) { + for (const prop in newValue) { + delete newValue[prop]; + } + setByPath(output, newPath, "[TRIMMEDOBJECT]"); + budget -= "[TRIMMEDOBJECT]".length; + } else { + queue.push({ obj: value, path: newPath }); + } + } + } else { + setByPath(output, newPath, value); + } + } + } + + return JSON.stringify(output); + + function setByPath( + obj: Serializable, + path: (string | number)[], + value: Serializable, + ): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let current: any = obj; // We'll refine this with type assertions as we traverse + for (let i = 0; i < path.length - 1; i++) { + if (typeof current[path[i]] === "undefined") { + current[path[i]] = typeof path[i + 1] === "number" ? [] : {}; + } + current = current[path[i]]; + } + current[path[path.length - 1]] = value; + } +} + +// Example usage: +// const obj = { +// name: "John", +// details: { +// age: 25, +// address: { +// street: "123 Main St", +// city: "Anytown", +// state: "CA", +// country: { +// name: "USA", +// code: "US", +// continent: { +// name: "North America", +// code: "NA", +// }, +// }, +// }, +// }, +// hobbies: ["reading", "traveling", "swimming", "hiking", "cycling"], +// bio: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", +// get fullName() { +// return this.name + " Doe"; +// }, +// }; + +// console.log(customStringify(obj, 200)); From f914978f0d4c85249b80c05afbe1e93d226134c9 Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 11 Oct 2023 23:12:56 +0200 Subject: [PATCH 2/6] ai fixes --- packages/editor/package.json | 2 + .../documentRenderers/richtext/FrameHost.tsx | 6 +- packages/editor/src/integrations/ai/openai.ts | 42 +++++ packages/frame/package.json | 6 +- packages/frame/src/Frame.tsx | 8 +- packages/frame/src/ai/ai.ts | 159 +++++++++++------- packages/frame/src/ai/applyChanges.ts | 25 ++- .../frame/src/codeblocks/MonacoCodeBlock.tsx | 1 + .../executor/components/ModelOutput.ts | 10 +- .../src/runtime/executor/lib/exports.tsx | 7 +- .../src/frameInterop/HostBridgeMethods.ts | 18 ++ 11 files changed, 207 insertions(+), 77 deletions(-) create mode 100644 packages/editor/src/integrations/ai/openai.ts diff --git a/packages/editor/package.json b/packages/editor/package.json index 741cf0141..6d5ea8050 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -38,6 +38,8 @@ "@typecell-org/parsers": "^0.0.3", "@typecell-org/frame": "^0.0.3", "@typecell-org/y-penpal": "^0.0.3", + "openai": "^4.11.1", + "ai": "2.2.14", "speakingurl": "^14.0.1", "classnames": "^2.3.1", "fractional-indexing": "^2.0.0", diff --git a/packages/editor/src/app/documentRenderers/richtext/FrameHost.tsx b/packages/editor/src/app/documentRenderers/richtext/FrameHost.tsx index 624082ba4..c9c1d781c 100644 --- a/packages/editor/src/app/documentRenderers/richtext/FrameHost.tsx +++ b/packages/editor/src/app/documentRenderers/richtext/FrameHost.tsx @@ -1,10 +1,11 @@ -import { IframeBridgeMethods } from "@typecell-org/shared"; +import { HostBridgeMethods, IframeBridgeMethods } from "@typecell-org/shared"; import { ContainedElement, useResource } from "@typecell-org/util"; import { PenPalProvider } from "@typecell-org/y-penpal"; import { AsyncMethodReturns, connectToChild } from "penpal"; import { useRef } from "react"; import * as awarenessProtocol from "y-protocols/awareness"; import { parseIdentifier } from "../../../identifiers"; +import { queryOpenAI } from "../../../integrations/ai/openai"; import { DocumentResource } from "../../../store/DocumentResource"; import { DocumentResourceModelProvider } from "../../../store/DocumentResourceModelProvider"; import { SessionStore } from "../../../store/local/SessionStore"; @@ -64,7 +65,7 @@ export function FrameHost(props: { { provider: DocumentResourceModelProvider; forwarder: ModelForwarder } >(); - const methods = { + const methods: HostBridgeMethods = { processYjsMessage: async (message: ArrayBuffer) => { provider.onMessage(message, "penpal"); }, @@ -110,6 +111,7 @@ export function FrameHost(props: { moduleManager.forwarder.dispose(); moduleManagers.delete(identifierStr); }, + queryLLM: queryOpenAI, }; const iframe = document.createElement("iframe"); diff --git a/packages/editor/src/integrations/ai/openai.ts b/packages/editor/src/integrations/ai/openai.ts new file mode 100644 index 000000000..56209a4af --- /dev/null +++ b/packages/editor/src/integrations/ai/openai.ts @@ -0,0 +1,42 @@ +import { OpenAIStream, StreamingTextResponse } from "ai"; +import { ChatCompletionCreateParamsBase, OpenAI } from "openai"; + +export async function queryOpenAI(parameters: { + messages: ChatCompletionCreateParamsBase["messages"]; + functions?: ChatCompletionCreateParamsBase["functions"]; + function_key?: ChatCompletionCreateParamsBase["function_key"]; +}) { + // get key from localstorage + let key = localStorage.getItem("oai-key"); + if (!key) { + key = prompt( + "Please enter your OpenAI key (not shared with TypeCell, stored in your browser):", + ); + if (!key) { + return { + status: "error", + error: "no-key", + } as const; + } + localStorage.setItem("oai-key", key); + } + + const openai = new OpenAI({ + apiKey: key, + dangerouslyAllowBrowser: true, + }); + + const response = await openai.chat.completions.create({ + model: "gpt-4", + stream: true, + ...parameters, + }); + const stream = OpenAIStream(response); + // Respond with the stream + const ret = new StreamingTextResponse(stream); + const data = await ret.text(); + return { + status: "ok", + result: data, + } as const; +} diff --git a/packages/frame/package.json b/packages/frame/package.json index 2d92732cd..b8732d01b 100644 --- a/packages/frame/package.json +++ b/packages/frame/package.json @@ -21,8 +21,6 @@ "mobx": "^6.2.0", "mobx-react-lite": "^3.2.0", "mobx-utils": "^6.0.8", - "openai": "^4.11.1", - "ai": "2.2.14", "prosemirror-model": "^1.19.3", "prosemirror-view": "^1.31.7", "prosemirror-state": "^1.4.3", @@ -48,7 +46,9 @@ "@vitest/coverage-v8": "^0.33.0", "@vitejs/plugin-react": "^4.1.0", "@types/prettier": "^3.0.0", - "chai": "^4.3.7" + "chai": "^4.3.7", + "openai": "^4.11.1", + "ai": "2.2.14" }, "type": "module", "source": "src/index.ts", diff --git a/packages/frame/src/Frame.tsx b/packages/frame/src/Frame.tsx index e8d59f496..f6765d1f6 100644 --- a/packages/frame/src/Frame.tsx +++ b/packages/frame/src/Frame.tsx @@ -302,7 +302,13 @@ export const Frame: React.FC = observer((props) => { execute: async (editor: BlockNoteEditor) => { const p = prompt("AI"); - const commands = await getAICode(p!, tools.newExecutionHost, editor); + const commands = await getAICode( + p!, + tools.newExecutionHost, + editor, + editorStore.current!, + connectionMethods.current!.queryLLM, + ); // debugger; // const commands = [ // { diff --git a/packages/frame/src/ai/ai.ts b/packages/frame/src/ai/ai.ts index 9b24917b4..75f21c52e 100644 --- a/packages/frame/src/ai/ai.ts +++ b/packages/frame/src/ai/ai.ts @@ -1,12 +1,13 @@ // import LocalExecutionHost from "../../../runtime/executor/executionHosts/local/LocalExecutionHost" import "@blocknote/core/style.css"; -import { OpenAIStream, StreamingTextResponse } from "ai"; import * as mobx from "mobx"; import * as monaco from "monaco-editor"; -import { OpenAI } from "openai"; +import { ChatCompletionMessageParam } from "openai"; import { BlockNoteEditor } from "@blocknote/core"; +import { HostBridgeMethods } from "@typecell-org/shared"; import { uri } from "vscode-lib"; +import { EditorStore } from "../EditorStore"; import { compile } from "../runtime/compiler/compilers/MonacoCompiler"; import { ExecutionHost } from "../runtime/executor/executionHosts/ExecutionHost"; import { customStringify } from "../stringify"; @@ -114,15 +115,12 @@ but instead: $.complexObject.newProperty = 5; `; -const openai = new OpenAI({ - apiKey: "", - dangerouslyAllowBrowser: true, -}); - export async function getAICode( prompt: string, executionHost: ExecutionHost, editor: BlockNoteEditor, + editorStore: EditorStore, + queryLLM: HostBridgeMethods["queryLLM"], ) { const models = monaco.editor.getModels(); const typeCellModels = models.filter((m) => @@ -130,19 +128,48 @@ export async function getAICode( ); const blocks = editor.topLevelBlocks; + let blockContexts: any[] = []; + const iterateBlocks = (blocks: any[]) => { + for (const block of blocks) { + const b = editorStore.getBlock(block.id); + if (b?.context?.default) { + blockContexts.push(b.context.default); + } + iterateBlocks(block.children); + } + }; + iterateBlocks(blocks); + + blockContexts = blockContexts.map((output) => + Object.fromEntries( + Object.getOwnPropertyNames(output).map((key) => [ + key, + mobx.toJS(output[key]), + ]), + ), + ); + const tmpModel = monaco.editor.createModel( "", "typescript", uri.URI.parse("file:///tmp.tsx"), ); - tmpModel.setValue(`import React from "!typecell:typecell.org/dqBLFEyFuSUu1"; - import * as $ from "!typecell:typecell.org/dqBLFEyFuSUu1"; + tmpModel.setValue(`import * as React from "react"; + import * as $ from "!typecell:typecell.org/dVeeYvbKcq2Nz"; // expands object types one level deep type Expand = T extends infer O ? { [K in keyof O]: O[K] extends { Key: React.Key | null } ? "[REACT]" : O[K] } : never; // expands object types recursively type ExpandRecursively = T extends object - ? T extends infer O ? { [K in keyof O]: O[K] extends { key: React.Key } ? "[REACT ELEMENT]" : ExpandRecursively } : never + ? T extends (...args: any[]) => any + ? T + : T extends infer O + ? { + [K in keyof O]: O[K] extends { key: React.Key } + ? "[REACT ELEMENT]" + : ExpandRecursively; + } + : never : T; // ? T extends infer O ? { [K in keyof O]: ExpandRecursively } : never @@ -158,7 +185,6 @@ type ExpandRecursively = T extends object const def2 = await ts.getQuickInfoAtPosition(tmpModel.uri.toString(), pos); const contextType = def2.displayParts.map((x: any) => x.text).join(""); // const def3 = await ts.get(tmpModel.uri.toString(), pos, {}); - tmpModel.dispose(); const codeInfoPromises = typeCellModels.map(async (m) => { @@ -212,7 +238,9 @@ type ExpandRecursively = T extends object if (block.children) { block.children = block.children.map(cleanBlock); } - block.content = block.content.map((x: any) => x.text).join(""); + if (Array.isArray(block.content)) { + block.content = block.content.map((x: any) => x.text).join(""); + } return block; } // console.log("request", JSON.stringify(blocks).length); @@ -225,48 +253,60 @@ type ExpandRecursively = T extends object contextType.replace("type ContextType = ", "const $: ") + " = " + JSON.stringify(outputJS); - // Ask OpenAI for a streaming chat completion given the prompt - const response = await openai.chat.completions.create({ - // model: "gpt-3.5-turbo-16k", - model: "gpt-4", - stream: true, - messages: [ - { - role: "system", - content: TYPECELL_PROMPT, - }, - { - role: "user", - content: `This is my document data: + + const blockContextInfo = blockContexts.length + ? `typecell.editor.findBlocks = (predicate: (context) => boolean) { + return (${JSON.stringify(blockContexts)}).find(predicate); + }` + : undefined; + + const messages: Array = [ + { + role: "system", + content: TYPECELL_PROMPT, + }, + { + role: "user", + content: `This is my document data: """${JSON.stringify(sanitized)}"""`, - }, - { - role: "user", - content: - "This is the type and runtime data available under the reactive $ variable for read / write access. If you need to change / read some information from the live document, it's likely you need to access it from here using $. \n" + - contextInfo, - }, - // codeInfos.length - // ? { - // role: "user", - // content: `This is the runtime / compiler data of the Code Blocks (CodeBlockRuntimeInfo[]): - // """${JSON.stringify(codeInfos)}"""`, - // } - // : { - // role: "user", - // content: `There are no code blocks in the document, so there's no runtime / compiler data for these (CodeBlockRuntimeInfo[]).`, - // }, - { - role: "system", - content: `You are an AI assistant helping user to modify his document. This means changes can either be code related (in that case, you'll need to add / modify Code Blocks), + }, + { + role: "user", + content: + "This is the type and runtime data available under the reactive $ variable for read / write access. If you need to change / read some information from the live document, it's likely you need to access it from here using $. \n" + + contextInfo + + (blockContextInfo + ? "\n" + + `We also have this function "typecell.editor.findBlocks" to extract runtime data from blocks \n` + + blockContextInfo + : ""), + }, + + // codeInfos.length + // ? { + // role: "user", + // content: `This is the runtime / compiler data of the Code Blocks (CodeBlockRuntimeInfo[]): + // """${JSON.stringify(codeInfos)}"""`, + // } + // : { + // role: "user", + // content: `There are no code blocks in the document, so there's no runtime / compiler data for these (CodeBlockRuntimeInfo[]).`, + // }, + { + role: "system", + content: `You are an AI assistant helping user to modify his document. This means changes can either be code related (in that case, you'll need to add / modify Code Blocks), or not at all (in which case you'll need to add / modify regular blocks), or a mix of both.`, - }, - { - role: "user", - content: prompt, // + - // " . \n\nRemember to reply ONLY with OperationsResponse JSON (DO NOT add any further comments). So start with [{ and end with }]", - }, - ], + }, + { + role: "user", + content: prompt, // + + // " . \n\nRemember to reply ONLY with OperationsResponse JSON (DO NOT add any further comments). So start with [{ and end with }]", + }, + ]; + + // Ask OpenAI for a streaming chat completion given the prompt + const response = await queryLLM({ + messages, functions: [ { name: "updateDocument", @@ -372,12 +412,13 @@ type ExpandRecursively = T extends object }, }); - const stream = OpenAIStream(response); - - // Respond with the stream - const ret = new StreamingTextResponse(stream); - const data = await ret.json(); - console.log(data); + console.log(messages); - return JSON.parse(data.function_call.arguments).operations; + if (response.status === "ok") { + const data = JSON.parse(response.result); + return JSON.parse(data.function_call.arguments).operations; + } else { + console.error("queryLLM error", response.error); + } + return undefined; } diff --git a/packages/frame/src/ai/applyChanges.ts b/packages/frame/src/ai/applyChanges.ts index 79ab87ec6..c804e3d52 100644 --- a/packages/frame/src/ai/applyChanges.ts +++ b/packages/frame/src/ai/applyChanges.ts @@ -1,4 +1,5 @@ import { error, uniqueId } from "@typecell-org/util"; +import * as ypm from "y-prosemirror"; import { Awareness } from "y-protocols/awareness"; import * as Y from "yjs"; import { BlockOperation, OperationsResponse } from "./types"; @@ -92,6 +93,9 @@ export async function applyChange( data: Y.XmlFragment, awareness: Awareness, ) { + const transact = (op: () => void) => { + Y.transact(data.doc!, op, ypm.ySyncPluginKey); + }; if (op.type === "add") { const node = findBlock(op.afterId, data); if (!node) { @@ -104,8 +108,9 @@ export async function applyChange( child.insert(0, [yText]); newElement.insert(0, [child]); // TODO: create block - (node.parent as Y.XmlElement).insertAfter(node, [newElement]); - + transact(() => { + (node.parent as Y.XmlElement).insertAfter(node, [newElement]); + }); // start typing text content for (let i = 0; i < op.content.length; i++) { const start = Y.createRelativePositionFromTypeIndex(yText, i); @@ -113,7 +118,9 @@ export async function applyChange( updateState(awareness, start, end); // return new RelativeSelection(start, end, sel.getDirection()) - yText.insert(i, op.content[i]); + transact(() => { + yText.insert(i, op.content[i]); + }); await new Promise((resolve) => setTimeout(resolve, 20)); } } else if (op.type === "delete") { @@ -131,7 +138,9 @@ export async function applyChange( await new Promise((resolve) => setTimeout(resolve, 200)); - (node.parent as Y.XmlElement).delete(findParentIndex(node), 1); + transact(() => { + (node.parent as Y.XmlElement).delete(findParentIndex(node), 1); + }); await new Promise((resolve) => setTimeout(resolve, 20)); } else if (op.type === "update") { const node = findBlock(op.id, data); @@ -160,7 +169,9 @@ export async function applyChange( updateState(awareness, start, end); // return new RelativeSelection(start, end, sel.getDirection()) - yText.insert(step.from + i, step.text[i]); + transact(() => { + yText.insert(step.from + i, step.text[i]); + }); await new Promise((resolve) => setTimeout(resolve, 20)); } // cell.code.delete(step.from, step.length); @@ -172,7 +183,9 @@ export async function applyChange( ); updateState(awareness, start, end); await new Promise((resolve) => setTimeout(resolve, 200)); - yText.delete(step.from, step.length); + transact(() => { + yText.delete(step.from, step.length); + }); await new Promise((resolve) => setTimeout(resolve, 20)); } } diff --git a/packages/frame/src/codeblocks/MonacoCodeBlock.tsx b/packages/frame/src/codeblocks/MonacoCodeBlock.tsx index 1b70379c7..affe60eb3 100644 --- a/packages/frame/src/codeblocks/MonacoCodeBlock.tsx +++ b/packages/frame/src/codeblocks/MonacoCodeBlock.tsx @@ -85,6 +85,7 @@ export const MonacoCodeBlock = createTipTapBlock<"codeblock", any>({ // class: styles.blockContent, "data-content-type": this.name, }), + 0, ]; }, diff --git a/packages/frame/src/runtime/executor/components/ModelOutput.ts b/packages/frame/src/runtime/executor/components/ModelOutput.ts index 816b9a11a..fef85ffdf 100644 --- a/packages/frame/src/runtime/executor/components/ModelOutput.ts +++ b/packages/frame/src/runtime/executor/components/ModelOutput.ts @@ -11,7 +11,9 @@ export class ModelOutput extends lifecycle.Disposable { private autorunDisposer: (() => void) | undefined; public value: any = undefined; - public _defaultValue: any = {}; + public _defaultValue = { + value: {} as any, + }; public typeVisualizers = observable.map< string, { @@ -21,6 +23,7 @@ export class ModelOutput extends lifecycle.Disposable { constructor(private context: any) { super(); + makeObservable(this, { typeVisualizers: observable.ref, value: observable.ref, @@ -70,13 +73,14 @@ export class ModelOutput extends lifecycle.Disposable { } } - this._defaultValue = newValue.default; + // hacky nesting to make sure our customAnnotation (for react elements) is used + this._defaultValue = { value: newValue.default }; if (changed) { if (Object.hasOwn(newValue, "default")) { Object.defineProperty(newValue, "default", { get: () => { - return this.defaultValue; + return this.defaultValue.value; }, }); } diff --git a/packages/frame/src/runtime/executor/lib/exports.tsx b/packages/frame/src/runtime/executor/lib/exports.tsx index dc0ade12c..7bbf7a83e 100644 --- a/packages/frame/src/runtime/executor/lib/exports.tsx +++ b/packages/frame/src/runtime/executor/lib/exports.tsx @@ -4,7 +4,7 @@ import memoize from "lodash.memoize"; import { autorun, comparer, computed, runInAction } from "mobx"; import { observer } from "mobx-react-lite"; import { computedFn, createTransformer } from "mobx-utils"; -import React, { useEffect, useMemo } from "react"; +import { useEffect, useMemo } from "react"; import { EditorStore } from "../../../EditorStore"; import { AutoForm, AutoFormProps } from "./autoForm"; import { Input } from "./input/Input"; @@ -125,7 +125,7 @@ export default function getExposeGlobalVariables( }, }; return { - memoize: (func: (...args: any[]) => any) => { + memoize: any>(func: T): T => { const wrapped = async function (this: any, ...args: any[]) { const ret = await func.apply(this, args); // if (typeof ret === "object") { @@ -135,7 +135,7 @@ export default function getExposeGlobalVariables( }; return memoize(wrapped, (args) => { return JSON.stringify(args); - }); + }) as any as T; }, // routing, // // DocumentView, @@ -195,6 +195,7 @@ export default function getExposeGlobalVariables( return undefined; } return autorun(() => { + // console.log("autorun setting", func); func(); }); }, diff --git a/packages/shared/src/frameInterop/HostBridgeMethods.ts b/packages/shared/src/frameInterop/HostBridgeMethods.ts index 6c2919c11..2364dfb39 100644 --- a/packages/shared/src/frameInterop/HostBridgeMethods.ts +++ b/packages/shared/src/frameInterop/HostBridgeMethods.ts @@ -18,4 +18,22 @@ export type HostBridgeMethods = { * Function for y-penpal */ processYjsMessage: (message: Uint8Array) => Promise; + + queryLLM: (parameters: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + messages: any[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + functions?: any[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function_call?: any; + }) => Promise< + | { + status: "ok"; + result: string; + } + | { + status: "error"; + error: string; + } + >; }; From b320e1d7d6dd7ca55873b01633f06fcb7c04639b Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 11 Oct 2023 23:33:39 +0200 Subject: [PATCH 3/6] fix build --- packages/editor/src/integrations/ai/openai.ts | 8 ++++---- packages/frame/src/ai/ai.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/editor/src/integrations/ai/openai.ts b/packages/editor/src/integrations/ai/openai.ts index 56209a4af..89d463890 100644 --- a/packages/editor/src/integrations/ai/openai.ts +++ b/packages/editor/src/integrations/ai/openai.ts @@ -1,10 +1,10 @@ import { OpenAIStream, StreamingTextResponse } from "ai"; -import { ChatCompletionCreateParamsBase, OpenAI } from "openai"; +import { OpenAI } from "openai"; export async function queryOpenAI(parameters: { - messages: ChatCompletionCreateParamsBase["messages"]; - functions?: ChatCompletionCreateParamsBase["functions"]; - function_key?: ChatCompletionCreateParamsBase["function_key"]; + messages: OpenAI.Chat.ChatCompletionCreateParams["messages"]; + functions?: OpenAI.Chat.ChatCompletionCreateParams["functions"]; + function_call?: OpenAI.Chat.ChatCompletionCreateParams["function_call"]; }) { // get key from localstorage let key = localStorage.getItem("oai-key"); diff --git a/packages/frame/src/ai/ai.ts b/packages/frame/src/ai/ai.ts index 75f21c52e..8eb3a5147 100644 --- a/packages/frame/src/ai/ai.ts +++ b/packages/frame/src/ai/ai.ts @@ -2,7 +2,7 @@ import "@blocknote/core/style.css"; import * as mobx from "mobx"; import * as monaco from "monaco-editor"; -import { ChatCompletionMessageParam } from "openai"; +import type openai from "openai"; import { BlockNoteEditor } from "@blocknote/core"; import { HostBridgeMethods } from "@typecell-org/shared"; @@ -260,7 +260,7 @@ type ExpandRecursively = T extends object }` : undefined; - const messages: Array = [ + const messages: openai.Chat.ChatCompletionCreateParams["messages"] = [ { role: "system", content: TYPECELL_PROMPT, From 699310fcf8411fd70943cda43aab5a73028bc773 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 17 Oct 2023 22:12:27 +0200 Subject: [PATCH 4/6] ai cleanup --- package-lock.json | 7 +- packages/editor/src/integrations/ai/openai.ts | 1 + packages/frame/package.json | 3 +- packages/frame/src/Frame.tsx | 76 ++++++++---------- packages/frame/src/ai/ai.ts | 78 ++++--------------- packages/frame/src/ai/applyChanges.ts | 40 +--------- .../{stringify.ts => ai/trimmedStringify.ts} | 11 ++- packages/frame/src/ai/types.ts | 1 + .../src/runtime/editor/compilerOptions.ts | 1 + .../executor/lib/autoForm/MonacoEdit.tsx | 2 +- .../src/runtime/executor/lib/exports.tsx | 1 - .../src/frameInterop/HostBridgeMethods.ts | 5 ++ 12 files changed, 75 insertions(+), 151 deletions(-) rename packages/frame/src/{stringify.ts => ai/trimmedStringify.ts} (89%) diff --git a/package-lock.json b/package-lock.json index 94a7d3ad3..49d7e1bc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16084,11 +16084,13 @@ "@typecell-org/shared": "^0.0.3", "@typecell-org/util": "^0.0.3", "@typecell-org/y-penpal": "^0.0.3", + "ai": "2.2.14", "classnames": "^2.3.1", "fractional-indexing": "^2.0.0", "lodash.memoize": "^4.1.2", "mobx": "^6.2.0", "mobx-react-lite": "^3.2.0", + "openai": "^4.11.1", "penpal": "^6.1.0", "react": "^18.2.0", "react-avatar": "^5.0.3", @@ -16503,7 +16505,6 @@ "@typecell-org/shared": "^0.0.3", "@typecell-org/util": "^0.0.3", "@typecell-org/y-penpal": "^0.0.3", - "ai": "2.2.14", "localforage": "^1.10.0", "lodash.memoize": "^4.1.2", "lz-string": "^1.4.4", @@ -16511,7 +16512,6 @@ "mobx-react-lite": "^3.2.0", "mobx-utils": "^6.0.8", "monaco-editor": "^0.35.0", - "openai": "^4.11.1", "penpal": "^6.1.0", "prettier": "^3.0.2", "prosemirror-keymap": "^1.2.2", @@ -16524,6 +16524,7 @@ "react-inspector": "^6.0.1", "typescript": "5.0.4", "vscode-lib": "^0.1.2", + "y-prosemirror": "^1.0.20", "y-protocols": "^1.0.5", "yjs": "^13.6.4" }, @@ -16532,9 +16533,11 @@ "@types/prettier": "^3.0.0", "@vitejs/plugin-react": "^4.1.0", "@vitest/coverage-v8": "^0.33.0", + "ai": "2.2.14", "chai": "^4.3.7", "cross-fetch": "^4.0.0", "jsdom": "^22.1.0", + "openai": "^4.11.1", "playwright-test": "^12.1.1", "typescript": "5.0.4", "vitest": "^0.33.0" diff --git a/packages/editor/src/integrations/ai/openai.ts b/packages/editor/src/integrations/ai/openai.ts index 89d463890..831a0fd2d 100644 --- a/packages/editor/src/integrations/ai/openai.ts +++ b/packages/editor/src/integrations/ai/openai.ts @@ -23,6 +23,7 @@ export async function queryOpenAI(parameters: { const openai = new OpenAI({ apiKey: key, + // this should be ok as we are not exposing any keys dangerouslyAllowBrowser: true, }); diff --git a/packages/frame/package.json b/packages/frame/package.json index b8732d01b..d559f14fd 100644 --- a/packages/frame/package.json +++ b/packages/frame/package.json @@ -34,7 +34,8 @@ "typescript": "5.0.4", "vscode-lib": "^0.1.2", "y-protocols": "^1.0.5", - "yjs": "^13.6.4" + "yjs": "^13.6.4", + "y-prosemirror": "^1.0.20" }, "devDependencies": { "cross-fetch": "^4.0.0", diff --git a/packages/frame/src/Frame.tsx b/packages/frame/src/Frame.tsx index f6765d1f6..52a507ffc 100644 --- a/packages/frame/src/Frame.tsx +++ b/packages/frame/src/Frame.tsx @@ -96,10 +96,11 @@ const originalItems = [ ...getDefaultReactSlashMenuItems(), { name: "Code block", - execute: (editor: any) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + execute: (editor: BlockNoteEditor) => insertOrUpdateBlock(editor, { type: "codeblock", - } as any), + }), aliases: ["code"], hint: "Add a live code block", group: "Code", @@ -107,7 +108,8 @@ const originalItems = [ }, { name: "Inline", - execute: (editor: any) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + execute: (editor: BlockNoteEditor) => { // state.tr.replaceSelectionWith(dinoType.create({type})) const node = editor._tiptapEditor.schema.node( "inlineCode", @@ -293,50 +295,45 @@ export const Frame: React.FC = observer((props) => { [props.documentIdString, monaco], ); - console.log("size", editorStore.current.customBlocks.size); slashMenuItems.splice( originalItems.length, slashMenuItems.length, { name: "AI", + // eslint-disable-next-line @typescript-eslint/no-explicit-any execute: async (editor: BlockNoteEditor) => { - const p = prompt("AI"); + const p = prompt("What would you like TypeCell AI to do?"); + if (!p) { + return; + } const commands = await getAICode( - p!, + p, tools.newExecutionHost, editor, - editorStore.current!, + editorStore.current, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion connectionMethods.current!.queryLLM, ); - // debugger; - // const commands = [ - // { - // // afterId: "3d70d0b1-02d7-4103-b145-452fafb93884", - // afterId: editor.topLevelBlocks[1].id, - // type: "add", - // content: - // "// This is a code block\nexport let value = 10;\nconsole.log(value);", - // blockType: "codeblock", - // } as const, - // ]; + + // TODO: we should validate the commands before applying them applyChanges( commands, document.ydoc.getXmlFragment("doc"), document.awareness, ); - // console.log(response); }, - aliases: ["ai", "magic"], - hint: "Prompt your AI code assistant", + aliases: ["ai", "wizard", "openai", "llm"], + hint: "Prompt your TypeCell AI assistant", group: "Code", icon: , }, - ...[...editorStore.current.customBlocks.values()].map((data: any) => { + ...[...editorStore.current.customBlocks.values()].map((data) => { console.log("update blocks"); return { name: data.name, - execute: (editor: any) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + execute: (editor: BlockNoteEditor) => { const origVarName = variables.toCamelCaseVariableName(data.name); let varName = origVarName; let i = 0; @@ -344,28 +341,23 @@ export const Frame: React.FC = observer((props) => { while (true) { // append _1, _2, _3, ... to the variable name until it is unique - if ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ( - tools.newExecutionHost.engine.observableContext - .rawContext as any - )[varName] === undefined - ) { + const context = + tools.newExecutionHost.engine.observableContext.rawContext; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((context as any)[varName] === undefined) { break; } i++; varName = origVarName + "_" + i; } - insertOrUpdateBlock( - editor as any, - { - type: "codeblock", - props: { - language: "typescript", - storage: "", - }, - content: `// @default-collapsed + insertOrUpdateBlock(editor, { + type: "codeblock", + props: { + language: "typescript", + storage: "", + }, + content: `// @default-collapsed import * as doc from "${data.documentId}"; export let ${varName} = doc.${data.blockVariable}; @@ -373,8 +365,7 @@ export let ${varName}Scope = doc; export default ${varName}; `, - } as any, - ); + }); }, // execute: (editor) => // insertOrUpdateBlock(editor, { @@ -383,7 +374,7 @@ export default ${varName}; // aliases: [data[0]], // hint: "Add a " + data[0], group: "Custom", - } as any; + }; }), ); @@ -436,6 +427,7 @@ export default ${varName}; }); if (editorStore.current.editor !== editor) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any editorStore.current.editor = editor as any; } diff --git a/packages/frame/src/ai/ai.ts b/packages/frame/src/ai/ai.ts index 8eb3a5147..935e8b8b3 100644 --- a/packages/frame/src/ai/ai.ts +++ b/packages/frame/src/ai/ai.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ // import LocalExecutionHost from "../../../runtime/executor/executionHosts/local/LocalExecutionHost" import "@blocknote/core/style.css"; import * as mobx from "mobx"; @@ -8,48 +9,8 @@ import { BlockNoteEditor } from "@blocknote/core"; import { HostBridgeMethods } from "@typecell-org/shared"; import { uri } from "vscode-lib"; import { EditorStore } from "../EditorStore"; -import { compile } from "../runtime/compiler/compilers/MonacoCompiler"; import { ExecutionHost } from "../runtime/executor/executionHosts/ExecutionHost"; -import { customStringify } from "../stringify"; -import { CodeBlockRuntimeInfo } from "./types"; - -// and for the runtime info: - -// /** -// * Runtime information about a code block of the main document -// * The code itself is not included (it's in the Block.id with the corresponding blockId) -// */ -// type MainCodeBlockRuntimeInfo = { -// imported: false; -// blockId: string; -// // .d.ts TypeScript types of values exported by this block -// types: string; -// // the runtime values exported by this block. Data can be trimmed for brevity -// // IMPORTANT: if this is for example { outputVariable: 5 }, it means the code block exports a variable \`outputVariable\` and the current value is 5 -// // You can access this reactive variable in other code blocks using the \`$\` variable. e.g.: \`$.outputVariable\` (it supports both reading and writing) -// data: any; -// }; - -// /** -// * Runtime + code information of code blocks imported from other documents -// */ -// type ImportedCodeBlockRuntimeInfo = { -// imported: true; -// /** -// * Because we don't pass the entire document this code is imported from, we need to pass the code of this code block -// */ -// code: string; -// // .d.ts TypeScript types of values exported by this block -// types: string; -// documentId: string; -// blockId: string; -// // the runtime values exported by this block. Data can be trimmed for brevity -// data: any; -// }; - -// export type CodeBlockRuntimeInfo = -// | MainCodeBlockRuntimeInfo -// | ImportedCodeBlockRuntimeInfo; +import { trimmedStringify } from "./trimmedStringify"; const TYPECELL_PROMPT = ` You're a smart AI assistant for TypeCell: a rich text document tool that also supports interactive Code Blocks written in Typescript. @@ -122,10 +83,6 @@ export async function getAICode( editorStore: EditorStore, queryLLM: HostBridgeMethods["queryLLM"], ) { - const models = monaco.editor.getModels(); - const typeCellModels = models.filter((m) => - m.uri.path.startsWith("/!typecell:typecell.org"), - ); const blocks = editor.topLevelBlocks; let blockContexts: any[] = []; @@ -187,6 +144,13 @@ type ExpandRecursively = T extends object // const def3 = await ts.get(tmpModel.uri.toString(), pos, {}); tmpModel.dispose(); + /* + // const models = monaco.editor.getModels(); + // const typeCellModels = models.filter((m) => + // m.uri.path.startsWith("/!typecell:typecell.org"), + // ); + + const codeInfoPromises = typeCellModels.map(async (m) => { const code = await compile(m, monaco); const output = await executionHost.outputs.get(m.uri.toString())?.value; @@ -218,7 +182,7 @@ type ExpandRecursively = T extends object return ret; }); let codeInfos = await Promise.all(codeInfoPromises); - codeInfos = codeInfos.filter((x) => !!x.imported); + codeInfos = codeInfos.filter((x) => !!x.imported);*/ const context = executionHost.engine.observableContext.rawContext as any; @@ -228,7 +192,7 @@ type ExpandRecursively = T extends object mobx.toJS(context[key]), ]), ); - outputJS = JSON.parse(customStringify(outputJS)); + outputJS = JSON.parse(trimmedStringify(outputJS)); function cleanBlock(block: any) { if (!block.content?.length && !block.children?.length) { @@ -243,12 +207,8 @@ type ExpandRecursively = T extends object } return block; } - // console.log("request", JSON.stringify(blocks).length); - const sanitized = blocks.map(cleanBlock).filter((x) => !!x); - // console.log("sanitized", JSON.stringify(sanitized).length); - // debugger; - // const command = prompt("prompt"); + const sanitized = blocks.map(cleanBlock).filter((x) => !!x); const contextInfo = contextType.replace("type ContextType = ", "const $: ") + " = " + @@ -281,17 +241,6 @@ type ExpandRecursively = T extends object blockContextInfo : ""), }, - - // codeInfos.length - // ? { - // role: "user", - // content: `This is the runtime / compiler data of the Code Blocks (CodeBlockRuntimeInfo[]): - // """${JSON.stringify(codeInfos)}"""`, - // } - // : { - // role: "user", - // content: `There are no code blocks in the document, so there's no runtime / compiler data for these (CodeBlockRuntimeInfo[]).`, - // }, { role: "system", content: `You are an AI assistant helping user to modify his document. This means changes can either be code related (in that case, you'll need to add / modify Code Blocks), @@ -299,8 +248,7 @@ type ExpandRecursively = T extends object }, { role: "user", - content: prompt, // + - // " . \n\nRemember to reply ONLY with OperationsResponse JSON (DO NOT add any further comments). So start with [{ and end with }]", + content: prompt, }, ]; diff --git a/packages/frame/src/ai/applyChanges.ts b/packages/frame/src/ai/applyChanges.ts index c804e3d52..5f39cad18 100644 --- a/packages/frame/src/ai/applyChanges.ts +++ b/packages/frame/src/ai/applyChanges.ts @@ -41,43 +41,9 @@ function updateState( cursor: { anchor, head, - // "anchor": { - // "type": { - // "client": 1521604366, - // "clock": 5 - // }, - // "tname": null, - // "item": { - // "client": 1521604366, - // "clock": 22 - // }, - // "assoc": 0 - // }, - // "head": { - // "type": { - // "client": 1521604366, - // "clock": 5 - // }, - // "tname": null, - // "item": { - // "client": 1521604366, - // "clock": 41 - // }, - // "assoc": 0 - // } }, }); - // if (!initial) { - // awareness.emit("update", [ - // { - // added: [99], - // updated: [], - // removed: [], - // }, - // origin, - // ]); - // } awareness.emit("change", [ { added: 0, @@ -94,6 +60,7 @@ export async function applyChange( awareness: Awareness, ) { const transact = (op: () => void) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion Y.transact(data.doc!, op, ypm.ySyncPluginKey); }; if (op.type === "add") { @@ -107,7 +74,7 @@ export async function applyChange( const yText = new Y.XmlText(); child.insert(0, [yText]); newElement.insert(0, [child]); - // TODO: create block + transact(() => { (node.parent as Y.XmlElement).insertAfter(node, [newElement]); }); @@ -116,7 +83,6 @@ export async function applyChange( const start = Y.createRelativePositionFromTypeIndex(yText, i); const end = Y.createRelativePositionFromTypeIndex(yText, i); updateState(awareness, start, end); - // return new RelativeSelection(start, end, sel.getDirection()) transact(() => { yText.insert(i, op.content[i]); @@ -167,14 +133,12 @@ export async function applyChange( step.from + i, ); updateState(awareness, start, end); - // return new RelativeSelection(start, end, sel.getDirection()) transact(() => { yText.insert(step.from + i, step.text[i]); }); await new Promise((resolve) => setTimeout(resolve, 20)); } - // cell.code.delete(step.from, step.length); } else if (step.type === "delete") { const start = Y.createRelativePositionFromTypeIndex(yText, step.from); const end = Y.createRelativePositionFromTypeIndex( diff --git a/packages/frame/src/stringify.ts b/packages/frame/src/ai/trimmedStringify.ts similarity index 89% rename from packages/frame/src/stringify.ts rename to packages/frame/src/ai/trimmedStringify.ts index 75c6a448c..ce263b5c3 100644 --- a/packages/frame/src/stringify.ts +++ b/packages/frame/src/ai/trimmedStringify.ts @@ -1,5 +1,11 @@ import React from "react"; +// quickly generated with chatgpt: +// "create a json stringify alternative that can trim long fields / deeply nested objects, +// and serializes property getters" + +// https://chat.openai.com/share/693c349f-3f74-4913-8f1d-ba4477f93e87 + type Serializable = | string | number @@ -13,7 +19,10 @@ interface QueueItem { path: (string | number)[]; } -export function customStringify(obj: Serializable, budget = 1000): string { +/** + * a stringify function that trims long fields / deeply nested objects, and serializes property getters + */ +export function trimmedStringify(obj: Serializable, budget = 1000): string { const seen = new Set(); const queue: QueueItem[] = [{ obj, path: [] }]; const output: Serializable = Array.isArray(obj) ? [] : {}; diff --git a/packages/frame/src/ai/types.ts b/packages/frame/src/ai/types.ts index baec50832..3a266f1b5 100644 --- a/packages/frame/src/ai/types.ts +++ b/packages/frame/src/ai/types.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /** * Operation to the document * diff --git a/packages/frame/src/runtime/editor/compilerOptions.ts b/packages/frame/src/runtime/editor/compilerOptions.ts index cba375504..63a505bd9 100644 --- a/packages/frame/src/runtime/editor/compilerOptions.ts +++ b/packages/frame/src/runtime/editor/compilerOptions.ts @@ -14,6 +14,7 @@ export function getDefaultSandboxCompilerOptions( >, ) { const settings: CompilerOptions = { + // used so that ts.getQuickInfoAtPosition doesn't truncate too soon noErrorTruncation: true, noImplicitAny: true, strictNullChecks: !config.useJavaScript, diff --git a/packages/frame/src/runtime/executor/lib/autoForm/MonacoEdit.tsx b/packages/frame/src/runtime/executor/lib/autoForm/MonacoEdit.tsx index e27c8f3a6..8e2c36147 100644 --- a/packages/frame/src/runtime/executor/lib/autoForm/MonacoEdit.tsx +++ b/packages/frame/src/runtime/executor/lib/autoForm/MonacoEdit.tsx @@ -11,7 +11,7 @@ type Props = { }; const MonacoEdit: React.FC = observer((props) => { - console.log(props); + // console.log(props); const uri = useMemo( () => monaco.Uri.parse(`${props.documentid}.edit.${Math.random()}.tsx`), diff --git a/packages/frame/src/runtime/executor/lib/exports.tsx b/packages/frame/src/runtime/executor/lib/exports.tsx index 7bbf7a83e..45a7fb4ca 100644 --- a/packages/frame/src/runtime/executor/lib/exports.tsx +++ b/packages/frame/src/runtime/executor/lib/exports.tsx @@ -195,7 +195,6 @@ export default function getExposeGlobalVariables( return undefined; } return autorun(() => { - // console.log("autorun setting", func); func(); }); }, diff --git a/packages/shared/src/frameInterop/HostBridgeMethods.ts b/packages/shared/src/frameInterop/HostBridgeMethods.ts index 2364dfb39..6ebb97732 100644 --- a/packages/shared/src/frameInterop/HostBridgeMethods.ts +++ b/packages/shared/src/frameInterop/HostBridgeMethods.ts @@ -19,6 +19,11 @@ export type HostBridgeMethods = { */ processYjsMessage: (message: Uint8Array) => Promise; + /** + * Function to query LLM (openai) + * Executed in host, so that the key can be stored in localstorage and + * cannot be accessed by user-scripts + */ queryLLM: (parameters: { // eslint-disable-next-line @typescript-eslint/no-explicit-any messages: any[]; From 9f65a335cf4290d85cdb0c41b72e46143e59efd9 Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 18 Oct 2023 20:05:32 +0200 Subject: [PATCH 5/6] ai fixes --- packages/engine/src/executor.ts | 1 + packages/engine/src/modules.ts | 10 ++++++---- packages/frame/src/Frame.tsx | 7 +------ packages/frame/src/ai/ai.ts | 6 +++++- .../executionHosts/local/LocalExecutionHost.tsx | 5 ++++- packages/server/package.json | 1 + 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index acbcc89ee..2e85c5908 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -120,6 +120,7 @@ export async function runModule( disposeEveryRun.push(hooks.disposeAll); let executionPromise: Promise; try { + console.log("execute", mod.factoryFunction + ""); executionPromise = mod.factoryFunction.apply( undefined, argsToCallFunctionWith, diff --git a/packages/engine/src/modules.ts b/packages/engine/src/modules.ts index 8a7506afe..2af8aa50f 100644 --- a/packages/engine/src/modules.ts +++ b/packages/engine/src/modules.ts @@ -28,10 +28,11 @@ export function getModulesFromWrappedPatchedTypeCellFunction( // eslint-disable-next-line @typescript-eslint/no-explicit-any caller: () => any, // eslint-disable-next-line @typescript-eslint/no-explicit-any - scope: any + scope: any, ): Module[] { const modules: Module[] = []; const define = createDefine(modules); + console.log("evaluate (module)", caller + ""); caller.apply({ ...scope, define }); return modules; } @@ -43,10 +44,11 @@ export function getModulesFromWrappedPatchedTypeCellFunction( export function getModulesFromPatchedTypeCellCode( code: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any - scope: any + scope: any, ): Module[] { const modules: Module[] = []; const define = createDefine(modules); + console.log("evaluate (module)", code); // eslint-disable-next-line const f = new Function(code); f.apply({ ...scope, define }); @@ -57,7 +59,7 @@ function createDefine(modules: Module[]) { return function typeCellDefine( moduleNameOrDependencyArray: string | string[], dependencyArrayOrFactoryFunction: string[] | Function, - factoryFunction?: Function + factoryFunction?: Function, ) { const moduleName: string | typeof unnamedModule = typeof moduleNameOrDependencyArray === "string" @@ -118,7 +120,7 @@ export function getPatchedTypeCellCode(compiledCode: string, scope: any) { totalCode = totalCode.replace( /^\s*(define\((".*", )?\[.*\], )function/gm, - "$1async function" + "$1async function", ); // TODO: remove await? return totalCode; diff --git a/packages/frame/src/Frame.tsx b/packages/frame/src/Frame.tsx index 52a507ffc..3dd19ee1f 100644 --- a/packages/frame/src/Frame.tsx +++ b/packages/frame/src/Frame.tsx @@ -309,6 +309,7 @@ export const Frame: React.FC = observer((props) => { const commands = await getAICode( p, + props.documentIdString, tools.newExecutionHost, editor, editorStore.current, @@ -367,12 +368,6 @@ export default ${varName}; `, }); }, - // execute: (editor) => - // insertOrUpdateBlock(editor, { - // type: data[0], - // }), - // aliases: [data[0]], - // hint: "Add a " + data[0], group: "Custom", }; }), diff --git a/packages/frame/src/ai/ai.ts b/packages/frame/src/ai/ai.ts index 935e8b8b3..c038981fd 100644 --- a/packages/frame/src/ai/ai.ts +++ b/packages/frame/src/ai/ai.ts @@ -78,6 +78,7 @@ $.complexObject.newProperty = 5; export async function getAICode( prompt: string, + documentId: string, executionHost: ExecutionHost, editor: BlockNoteEditor, editorStore: EditorStore, @@ -111,8 +112,9 @@ export async function getAICode( "typescript", uri.URI.parse("file:///tmp.tsx"), ); + tmpModel.setValue(`import * as React from "react"; - import * as $ from "!typecell:typecell.org/dVeeYvbKcq2Nz"; + import * as $ from "!${documentId}"; // expands object types one level deep type Expand = T extends infer O ? { [K in keyof O]: O[K] extends { Key: React.Key | null } ? "[REACT]" : O[K] } : never; @@ -140,7 +142,9 @@ type ExpandRecursively = T extends object tmpModel.getValue().length - "pe = ExpandRecursively;".length; // const def = await ts.getDefinitionAtPosition(tmpModel.uri.toString(), pos); const def2 = await ts.getQuickInfoAtPosition(tmpModel.uri.toString(), pos); + const contextType = def2.displayParts.map((x: any) => x.text).join(""); + // const def3 = await ts.get(tmpModel.uri.toString(), pos, {}); tmpModel.dispose(); diff --git a/packages/frame/src/runtime/executor/executionHosts/local/LocalExecutionHost.tsx b/packages/frame/src/runtime/executor/executionHosts/local/LocalExecutionHost.tsx index 727c1e15b..a2552e8b8 100644 --- a/packages/frame/src/runtime/executor/executionHosts/local/LocalExecutionHost.tsx +++ b/packages/frame/src/runtime/executor/executionHosts/local/LocalExecutionHost.tsx @@ -35,7 +35,10 @@ export default class LocalExecutionHost // ); // }) // ); - this.engine.registerModelProvider(compileEngine); + + if (!window.location.hash.includes("noRun")) { + this.engine.registerModelProvider(compileEngine); + } const visualizerExtension = this._register( new VisualizerExtension(compileEngine, documentId, monacoInstance), diff --git a/packages/server/package.json b/packages/server/package.json index e103e667f..ef6473dd0 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -34,6 +34,7 @@ "start": "node dist/src/index.js", "start:supabase": "./supabase.sh start", "stop:supabase": "./supabase.sh stop", + "dump": "./supabase.sh db dump -f export.sql --db-url=postgresql://postgres:postgres@localhost:54322 --data-only", "clean": "rimraf dist && rimraf types", "dev": "MODE=development vite-node src/index.ts", "build": "npm run clean && tsc -p tsconfig.json", From e27227ec5cd270d6665224e283c1c9214cb24771 Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 18 Oct 2023 22:57:44 +0200 Subject: [PATCH 6/6] fix settings (autoform), albeit somewhat hacky --- packages/engine/src/executor.ts | 2 + packages/frame/src/EditorStore.ts | 34 +++++++++--- packages/frame/src/Frame.tsx | 18 ++++++- packages/frame/src/RichTextContext.ts | 4 ++ .../frame/src/codeblocks/MonacoElement.tsx | 52 +++++++++++++------ .../executor/lib/autoForm/FormField.tsx | 8 +-- .../src/runtime/executor/lib/exports.tsx | 36 +++++++++++-- 7 files changed, 123 insertions(+), 31 deletions(-) diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index 2e85c5908..79f222f54 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -13,6 +13,7 @@ async function resolveDependencyArray( userDisposes: Array<() => void>, ) { const runContext = { + context, onDispose: (disposer: () => void) => { userDisposes.push(() => { try { @@ -47,6 +48,7 @@ async function resolveDependencyArray( } export type RunContext = { + context: TypeCellContext; onDispose: (disposer: () => void) => void; }; diff --git a/packages/frame/src/EditorStore.ts b/packages/frame/src/EditorStore.ts index da4b03ba2..8487ab710 100644 --- a/packages/frame/src/EditorStore.ts +++ b/packages/frame/src/EditorStore.ts @@ -23,11 +23,17 @@ export class EditorStore { public executionHost: LocalExecutionHost | undefined; public topLevelBlocks: any; + public readonly customBlocks = new Map(); + public readonly blockSettings = new Map(); + constructor() { makeObservable(this, { customBlocks: observable.shallow, - add: action, - delete: action, + addCustomBlock: action, + deleteCustomBlock: action, + blockSettings: observable.shallow, + addBlockSettings: action, + deleteBlockSettings: action, topLevelBlocks: observable.ref, }); @@ -45,12 +51,10 @@ export class EditorStore { }); } - customBlocks = new Map(); - /** * Add a custom block (slash menu command) to the editor */ - public add(config: any) { + public addCustomBlock(config: any) { if (this.customBlocks.has(config.id)) { // already has block with this id, maybe loop of documents? return; @@ -61,10 +65,28 @@ export class EditorStore { /** * Remove a custom block (slash menu command) from the editor */ - public delete(config: any) { + public deleteCustomBlock(config: any) { this.customBlocks.delete(config.id); } + /** + * Add a block settings (block settings menu) to the editor + */ + public addBlockSettings(config: any) { + if (this.blockSettings.has(config.id)) { + // already has block with this id, maybe loop of documents? + return; + } + this.blockSettings.set(config.id, config); + } + + /** + * Remove block settings (block settings menu) from the editor + */ + public deleteBlockSettings(config: any) { + this.blockSettings.delete(config.id); + } + /** * EXPERIMENTAL * @internal diff --git a/packages/frame/src/Frame.tsx b/packages/frame/src/Frame.tsx index 3dd19ee1f..c6edf1bef 100644 --- a/packages/frame/src/Frame.tsx +++ b/packages/frame/src/Frame.tsx @@ -352,6 +352,19 @@ export const Frame: React.FC = observer((props) => { varName = origVarName + "_" + i; } + const settingsPart = data.settings + ? ` +typecell.editor.registerBlockSettings({ + content: (visible: boolean) => ( + + ), +}); +` + : ""; insertOrUpdateBlock(editor, { type: "codeblock", props: { @@ -361,9 +374,9 @@ export const Frame: React.FC = observer((props) => { content: `// @default-collapsed import * as doc from "${data.documentId}"; -export let ${varName} = doc.${data.blockVariable}; +export let ${varName} = doc.${data.blockExport}; export let ${varName}Scope = doc; - +${settingsPart} export default ${varName}; `, }); @@ -435,6 +448,7 @@ export default ${varName}; ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + editorStore: undefined as any, // eslint-disable-next-line @typescript-eslint/no-explicit-any executionHost: undefined as any, // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/frame/src/codeblocks/MonacoElement.tsx b/packages/frame/src/codeblocks/MonacoElement.tsx index 60251cff0..527790ace 100644 --- a/packages/frame/src/codeblocks/MonacoElement.tsx +++ b/packages/frame/src/codeblocks/MonacoElement.tsx @@ -10,7 +10,11 @@ import React, { useRef, useState, } from "react"; -import { VscChevronDown, VscChevronRight } from "react-icons/vsc"; +import { + VscChevronDown, + VscChevronRight, + VscSettingsGear, +} from "react-icons/vsc"; import { autoUpdate, @@ -261,9 +265,13 @@ const MonacoBlockElement = ( const [codeVisible, setCodeVisible] = useState( () => props.node.textContent.startsWith("// @default-collapsed") === false, ); + const [settingsVisible, setSettingsVisible] = useState(false); const context = useContext(RichTextContext); + const settings = context.editorStore.blockSettings.get( + props.model.uri.toString(), + ); return (
- {codeVisible ? ( - setCodeVisible(false)} - /> - ) : ( - setCodeVisible(true)} - /> - )} - {} +
+ {codeVisible ? ( + setCodeVisible(false)} + /> + ) : ( + setCodeVisible(true)} + /> + )} + {settings && ( + setSettingsVisible(!settingsVisible)} + /> + )} +
{codeVisible && (
@@ -309,6 +330,7 @@ const MonacoBlockElement = ( }, )}
+ {settings &&
{settings.content(settingsVisible)}
}
); diff --git a/packages/frame/src/runtime/executor/lib/autoForm/FormField.tsx b/packages/frame/src/runtime/executor/lib/autoForm/FormField.tsx index a58e93862..c0c02e126 100644 --- a/packages/frame/src/runtime/executor/lib/autoForm/FormField.tsx +++ b/packages/frame/src/runtime/executor/lib/autoForm/FormField.tsx @@ -57,6 +57,8 @@ export const FormField = observer( let inputField: React.ReactNode =
Unsupported type
; + const realShowCode = showCode || !canUseInput; + if (canUseInput) { const valueType = currentParsedBinding === undefined @@ -112,7 +114,7 @@ export const FormField = observer( {({ fieldProps, error }) => (
- {showCode ? ( + {realShowCode ? ( setShowCode(false)} - isSelected={!showCode} + isSelected={!realShowCode} isDisabled={!canUseInput}> Value view setShowCode(true)} - isSelected={showCode}> + isSelected={realShowCode}> Code view diff --git a/packages/frame/src/runtime/executor/lib/exports.tsx b/packages/frame/src/runtime/executor/lib/exports.tsx index 45a7fb4ca..2f2194b69 100644 --- a/packages/frame/src/runtime/executor/lib/exports.tsx +++ b/packages/frame/src/runtime/executor/lib/exports.tsx @@ -21,7 +21,11 @@ export default function getExposeGlobalVariables( // registerPlugin: (config: { name: string }) => { // return config.name; // }, - registerBlock: (config: { name: string; blockExport: string }) => { + registerBlock: (config: { + name: string; + blockExport: string; + settings?: Record; + }) => { // TODO: this logic should be part of CodeModel / BasicCodeModel const id = forModelList[forModelList.length - 1].path; const parts = decodeURIComponent(id.replace("file:///", "")).split("/"); @@ -35,11 +39,27 @@ export default function getExposeGlobalVariables( documentId, }; console.log("ADD BLOCK", completeConfig.id); - editorStore.add(completeConfig); + editorStore.addCustomBlock(completeConfig); runContext.onDispose(() => { console.log("REMOVE BLOCK", completeConfig.id); - editorStore.delete(completeConfig); + editorStore.deleteCustomBlock(completeConfig); + }); + }, + registerBlockSettings: (config: any) => { + // TODO: this logic should be part of CodeModel / BasicCodeModel + const id = forModelList[forModelList.length - 1].uri; + + const completeConfig: any = { + ...config, + id: id.toString(), + }; + // console.log("ADD BLOCK", completeConfig.id); + editorStore.addBlockSettings(completeConfig); + + runContext.onDispose(() => { + // console.log("REMOVE BLOCK", completeConfig.id); + editorStore.deleteBlockSettings(completeConfig); }); }, /** @@ -163,7 +183,9 @@ export default function getExposeGlobalVariables( [key: string]: unknown; }, >( - props: Exclude, "settings" | "setSettings">, + props: Exclude, "settings" | "setSettings"> & { + visible: boolean; + }, ) => { const storage = editor.currentBlock.storage; @@ -182,7 +204,7 @@ export default function getExposeGlobalVariables( const func = new Function("$target", "$", sanitizedCode); return () => { console.log("eval", key, code); - func(props.inputObject, props.inputObject); // TODO + func(props.inputObject, runContext.context.context); }; }); }, [props.inputObject, storage.settings]); @@ -212,6 +234,10 @@ export default function getExposeGlobalVariables( }); }, [props.fields, createFunctionTransformer]); + if (!props.visible) { + return <>; + } + return (