From e0a7d15d1cdd9b2a98695f2f4a92a9f12da3023b Mon Sep 17 00:00:00 2001 From: yousefed Date: Fri, 6 Oct 2023 10:31:06 +0200 Subject: [PATCH] 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));