From fdb96b6ae18a2c1e704276f75e8980a3b79af0b8 Mon Sep 17 00:00:00 2001 From: Jeff-Emmett Date: Thu, 23 Jan 2025 14:14:04 +0100 Subject: [PATCH] slidedeck shape installed, still minor difficulty in keyboard arrow transition between slides (last slide + wraparound) --- package-lock.json | 399 +++++++++++++++++++++++++++++++++- package.json | 3 + src/lib/settings.tsx | 61 ++++++ src/prompt.ts | 24 ++ src/routes/Board.tsx | 124 ++++++++++- src/shapes/SlideShapeUtil.tsx | 128 +++++++++++ src/slides/SlidesPanel.tsx | 37 ++++ src/slides/slides.css | 32 +++ src/slides/useSlides.tsx | 31 +++ src/tools/SlideShapeTool.ts | 12 + src/ui/CustomContextMenu.tsx | 20 +- src/ui/CustomToolbar.tsx | 8 + src/ui/SettingsDialog.tsx | 198 +++++++++++++++++ src/ui/overrides.tsx | 23 +- worker/TldrawDurableObject.ts | 9 +- 15 files changed, 1086 insertions(+), 23 deletions(-) create mode 100644 src/lib/settings.tsx create mode 100644 src/prompt.ts create mode 100644 src/shapes/SlideShapeUtil.tsx create mode 100644 src/slides/SlidesPanel.tsx create mode 100644 src/slides/slides.css create mode 100644 src/slides/useSlides.tsx create mode 100644 src/tools/SlideShapeTool.ts create mode 100644 src/ui/SettingsDialog.tsx diff --git a/package-lock.json b/package-lock.json index 2c6341c..31e3e11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@anthropic-ai/sdk": "^0.33.1", "@daily-co/daily-js": "^0.60.0", "@daily-co/daily-react": "^0.20.0", "@tldraw/assets": "^3.6.0", @@ -19,6 +20,7 @@ "@types/markdown-it": "^14.1.1", "@types/marked": "^5.0.2", "@vercel/analytics": "^1.2.2", + "ai": "^4.1.0", "cherry-markdown": "^0.8.57", "cloudflare-workers-unfurl": "^0.0.7", "gray-matter": "^4.0.3", @@ -28,6 +30,7 @@ "jspdf": "^2.5.2", "lodash.throttle": "^4.1.1", "marked": "^15.0.4", + "openai": "^4.79.3", "rbush": "^4.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -53,6 +56,90 @@ "node": ">=18.0.0" } }, + "node_modules/@ai-sdk/provider": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.0.4.tgz", + "integrity": "sha512-lJi5zwDosvvZER3e/pB8lj1MN3o3S7zJliQq56BRr4e9V3fcRyFtwP0JRxaRS5vHYX3OJ154VezVoQNrk0eaKw==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.1.0.tgz", + "integrity": "sha512-rBUabNoyB25PBUjaiMSk86fHNSCqTngNZVvXxv8+6mvw47JX5OexW+ZHRsEw8XKTE8+hqvNFVzctaOrRZ2i9Zw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.0.4", + "eventsource-parser": "^3.0.0", + "nanoid": "^3.3.8", + "secure-json-parse": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@ai-sdk/react": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-1.1.0.tgz", + "integrity": "sha512-U5lBbLyf1pw79xsk5dgHSkBv9Jta3xzWlOLpxsmHlxh1X94QOH3e1gm+nioQ/JvTuHLm23j2tz3i4MpMdchwXQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "2.1.0", + "@ai-sdk/ui-utils": "1.1.0", + "swr": "^2.2.5", + "throttleit": "2.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "zod": "^3.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@ai-sdk/ui-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.1.0.tgz", + "integrity": "sha512-ETXwdHaHwzC7NIehbthDFGwsTFk+gNtRL/lm85nR4WDFvvYQptoM/7wTANs0p0H7zumB3Ep5hKzv0Encu8vSRw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.0.4", + "@ai-sdk/provider-utils": "2.1.0", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -67,6 +154,30 @@ "node": ">=6.0.0" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.33.1.tgz", + "integrity": "sha512-VrlbxiAdVRGuKP2UQlCnsShDHJKWepzvfRCkZMpU+oaUdKLpOfmylLMRojGrAgebV+kDtPjewCVP0laHXg+vsA==", + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.71", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.71.tgz", + "integrity": "sha512-evXpcgtZm8FY4jqBSN8+DmOTcVkkvTmAayeo4Wf3m1xAruyVGzGuDh/Fb/WWX2yLItUiho42ozyJjB0dw//Tkw==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -1318,6 +1429,15 @@ "node": ">= 8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", @@ -2765,6 +2885,12 @@ "integrity": "sha512-VgnAj6tIAhJhZdJ8/IpxdatM8G4OD3VWGlp6xIxUGENZlpbob9Ty4VVdC1FIEp0aK6DBscDDjyzy5FB60TuNqg==", "license": "MIT" }, + "node_modules/@types/diff-match-patch": { + "version": "1.0.36", + "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz", + "integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==", + "license": "MIT" + }, "node_modules/@types/dompurify": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz", @@ -2837,6 +2963,16 @@ "integrity": "sha512-3oJbGBUWuS6ahSnEq1eN2XrCyf4YsWI8OyCvo7c64zQJNplk3mO84t53o8lfTk+2ji59g5ycfc6qQ3fdHliHuA==", "license": "MIT" }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/node-forge": { "version": "1.3.11", "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", @@ -3387,6 +3523,18 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "license": "ISC" }, + "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==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -3451,6 +3599,47 @@ "node": ">= 6.0.0" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/ai": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ai/-/ai-4.1.0.tgz", + "integrity": "sha512-95nI9hBSSAKPrMnpJbaB3yqvh+G8BS4/EtFz3HR0HgEDJpxC0R6JAlB8+B/BXHd/roNGBrS08Z3Zain/6OFSYA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.0.4", + "@ai-sdk/provider-utils": "2.1.0", + "@ai-sdk/react": "1.1.0", + "@ai-sdk/ui-utils": "1.1.0", + "@opentelemetry/api": "1.9.0", + "jsondiffpatch": "0.6.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "zod": "^3.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4742,6 +4931,12 @@ "node": ">=0.3.1" } }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "license": "Apache-2.0" + }, "node_modules/domexception": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", @@ -5280,6 +5475,15 @@ "node": ">= 0.6" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -5301,6 +5505,15 @@ "integrity": "sha512-blk1va0zol9QOrdZt0rFXo5KMkNPVSp92Eju/Qz8THwKWKRKeE0T8Br/1aW6+Edkyq9xHYgYxn2QtOnUKPUp+Q==", "license": "MIT" }, + "node_modules/eventsource-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", + "integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/execa/-/execa-3.2.0.tgz", @@ -5438,6 +5651,25 @@ "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, "node_modules/fp-ts": { "version": "2.16.9", "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.9.tgz", @@ -5800,6 +6032,15 @@ "node": ">=8.12.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==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -6076,6 +6317,12 @@ "node": ">=6" } }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json-schema-to-ts": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-1.6.4.tgz", @@ -6106,6 +6353,35 @@ "node": ">=6" } }, + "node_modules/jsondiffpatch": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz", + "integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==", + "license": "MIT", + "dependencies": { + "@types/diff-match-patch": "^1.0.36", + "chalk": "^5.3.0", + "diff-match-patch": "^1.0.5" + }, + "bin": { + "jsondiffpatch": "bin/jsondiffpatch.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/jsondiffpatch/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -6529,7 +6805,6 @@ "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "dev": true, "funding": [ { "type": "github", @@ -6544,6 +6819,25 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -6707,6 +7001,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "4.79.3", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.79.3.tgz", + "integrity": "sha512-0yAnr6oxXAyVrYwLC1jA0KboyU7DjEmrfTXQX+jSpE+P4i72AI/Lxx5pvR3r9i5X7G33835lL+ZrnQ+MDvyuUg==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.71", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.71.tgz", + "integrity": "sha512-evXpcgtZm8FY4jqBSN8+DmOTcVkkvTmAayeo4Wf3m1xAruyVGzGuDh/Fb/WWX2yLItUiho42ozyJjB0dw//Tkw==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/os-paths": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/os-paths/-/os-paths-4.4.0.tgz", @@ -7498,6 +7831,12 @@ "node": ">=4" } }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, "node_modules/selfsigned": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", @@ -7803,6 +8142,19 @@ "node": ">=12.0.0" } }, + "node_modules/swr": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.0.tgz", + "integrity": "sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -7836,6 +8188,18 @@ "utrie": "^1.0.2" } }, + "node_modules/throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/time-span": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/time-span/-/time-span-4.0.0.tgz", @@ -8066,6 +8430,12 @@ "node": ">=14.0" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, "node_modules/unenv": { "name": "unenv-nightly", "version": "2.0.0-20241204-140205-a5d5190", @@ -8191,6 +8561,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -8388,6 +8767,15 @@ "node": ">=12" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/web-vitals": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-0.2.4.tgz", @@ -9179,6 +9567,15 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz", + "integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } } } } diff --git a/package.json b/package.json index 7b1a8db..20684fd 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "author": "Jeff Emmett", "license": "ISC", "dependencies": { + "@anthropic-ai/sdk": "^0.33.1", "@daily-co/daily-js": "^0.60.0", "@daily-co/daily-react": "^0.20.0", "@tldraw/assets": "^3.6.0", @@ -25,6 +26,7 @@ "@types/markdown-it": "^14.1.1", "@types/marked": "^5.0.2", "@vercel/analytics": "^1.2.2", + "ai": "^4.1.0", "cherry-markdown": "^0.8.57", "cloudflare-workers-unfurl": "^0.0.7", "gray-matter": "^4.0.3", @@ -34,6 +36,7 @@ "jspdf": "^2.5.2", "lodash.throttle": "^4.1.1", "marked": "^15.0.4", + "openai": "^4.79.3", "rbush": "^4.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/lib/settings.tsx b/src/lib/settings.tsx new file mode 100644 index 0000000..ca8c1a0 --- /dev/null +++ b/src/lib/settings.tsx @@ -0,0 +1,61 @@ +import { atom } from 'tldraw' +import { SYSTEM_PROMPT } from '@/prompt' + +export const PROVIDERS = [ + { + id: 'openai', + name: 'OpenAI', + models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo'], // 'o1-preview', 'o1-mini'], + help: 'https://tldraw.notion.site/Make-Real-Help-93be8b5273d14f7386e14eb142575e6e#a9b75e58b1824962a1a69a2f29ace9be', + validate: (key: string) => key.startsWith('sk-'), + }, + { + id: 'anthropic', + name: 'Anthropic', + models: [ + 'claude-3-5-sonnet-20241022', + 'claude-3-5-sonnet-20240620', + 'claude-3-opus-20240229', + 'claude-3-sonnet-20240229', + 'claude-3-haiku-20240307', + ], + help: 'https://tldraw.notion.site/Make-Real-Help-93be8b5273d14f7386e14eb142575e6e#3444b55a2ede405286929956d0be6e77', + validate: (key: string) => key.startsWith('sk-'), + }, + // { id: 'google', name: 'Google', model: 'Gemeni 1.5 Flash', validate: (key: string) => true }, +] + +export const makeRealSettings = atom('make real settings', { + provider: 'openai' as (typeof PROVIDERS)[number]['id'] | 'all', + models: Object.fromEntries(PROVIDERS.map((provider) => [provider.id, provider.models[0]])), + keys: { + openai: '', + anthropic: '', + google: '', + }, + prompts: { + system: SYSTEM_PROMPT, + }, +}) + +export function applySettingsMigrations(settings: any) { + const { keys, prompts, ...rest } = settings + + const settingsWithModelsProperty = { + provider: 'openai', + models: Object.fromEntries(PROVIDERS.map((provider) => [provider.id, provider.models[0]])), + keys: { + openai: '', + anthropic: '', + google: '', + ...keys, + }, + prompts: { + system: SYSTEM_PROMPT, + ...prompts, + }, + ...rest, + } + + return settingsWithModelsProperty +} \ No newline at end of file diff --git a/src/prompt.ts b/src/prompt.ts new file mode 100644 index 0000000..5270b26 --- /dev/null +++ b/src/prompt.ts @@ -0,0 +1,24 @@ +export const SYSTEM_PROMPT = `You are an expert web developer who specializes in building working website prototypes from low-fidelity wireframes. Your job is to accept low-fidelity designs and turn them into high-fidelity interactive and responsive working prototypes. When sent new designs, you should reply with a high-fidelity working prototype as a single HTML file. + +- Use tailwind (via \`cdn.tailwindcss.com\`) for styling. +- Put any JavaScript in a script tag with \`type="module"\`. +- Use unpkg or skypack to import any required JavaScript dependencies. +- Use Google fonts to pull in any open source fonts you require. +- If you have any images, load them from Unsplash or use solid colored rectangles as placeholders. +- Create SVGs as needed for any icons. + +The designs may include flow charts, diagrams, labels, arrows, sticky notes, screenshots of other applications, or even previous designs. Treat all of these as references for your prototype. + +The designs may include structural elements (such as boxes that represent buttons or content) as well as annotations or figures that describe interactions, behavior, or appearance. Use your best judgement to determine what is an annotation and what should be included in the final result. Annotations are commonly made in the color red. Do NOT include any of those annotations in your final result. + +If there are any questions or underspecified features, use what you know about applications, user experience, and website design patterns to "fill in the blanks". If you're unsure of how the designs should work, take a guess—it's better for you to get it wrong than to leave things incomplete. + +Your prototype should look and feel much more complete and advanced than the wireframes provided. Flesh it out, make it real! + +Remember: you love your designers and want them to be happy. The more complete and impressive your prototype, the happier they will be. You are evaluated on 1) whether your prototype resembles the designs, 2) whether your prototype is interactive and responsive, and 3) whether your prototype is complete and impressive.` + +export const USER_PROMPT = + 'Here are the latest wireframes. Please reply with a high-fidelity working prototype as a single HTML file.' + +export const USER_PROMPT_WITH_PREVIOUS_DESIGN = + "Here are the latest wireframes. There are also some previous outputs here. We have run their code through an 'HTML to screenshot' library to generate a screenshot of the page. The generated screenshot may have some inaccuracies so please use your knowledge of HTML and web development to figure out what any annotations are referring to, which may be different to what is visible in the generated screenshot. Make a new high-fidelity prototype based on your previous work and any new designs or annotations. Again, you should reply with a high-fidelity working prototype as a single HTML file." \ No newline at end of file diff --git a/src/routes/Board.tsx b/src/routes/Board.tsx index 7a073ef..2afe4e0 100644 --- a/src/routes/Board.tsx +++ b/src/routes/Board.tsx @@ -1,6 +1,6 @@ import { useSync } from "@tldraw/sync" import { useMemo } from "react" -import { Tldraw, Editor } from "tldraw" +import { Tldraw, Editor, useTools, useIsToolSelected, DefaultToolbar, TldrawUiMenuItem, DefaultToolbarContent, DefaultKeyboardShortcutsDialog, DefaultKeyboardShortcutsDialogContent, defaultTools } from "tldraw" import { useParams } from "react-router-dom" import { ChatBoxTool } from "@/tools/ChatBoxTool" import { ChatBoxShape } from "@/shapes/ChatBoxShapeUtil" @@ -20,25 +20,122 @@ import { handleInitialPageLoad } from "@/utils/handleInitialPageLoad" import { MycrozineTemplateTool } from "@/tools/MycrozineTemplateTool" import { MycrozineTemplateShape } from "@/shapes/MycrozineTemplateShapeUtil" import { registerPropagators, ChangePropagator, TickPropagator, ClickPropagator } from "@/propagators/ScopedPropagators" +import { SlideShapeTool } from "@/tools/SlideShapeTool" +import { ISlideShape, SlideShapeUtil } from "@/shapes/SlideShapeUtil" +import { SlidesPanel } from "@/slides/SlidesPanel" +import { moveToSlide } from "@/slides/useSlides" // Default to production URL if env var isn't available export const WORKER_URL = "https://jeffemmett-canvas.jeffemmett.workers.dev" -const shapeUtils = [ +const updatedComponents = { + ...components, + HelperButtons: SlidesPanel, + Minimap: null, + Toolbar: (props: any) => { + const tools = useTools() + const slideTool = tools['Slide'] + const isSlideSelected = slideTool ? useIsToolSelected(slideTool) : false + return ( + + {slideTool && } + + + ) + }, + KeyboardShortcutsDialog: (props: any) => { + const tools = useTools() + return ( + + + + + ) + }, +} + +const customShapeUtils = [ ChatBoxShape, VideoChatShape, EmbedShape, - // MycrozineTemplateShape, - // MarkdownShape + SlideShapeUtil, + MycrozineTemplateShape, + MarkdownShape ] -const tools = [ +const customTools = [ ChatBoxTool, VideoChatTool, EmbedTool, - // MycrozineTemplateTool, - // MarkdownTool + SlideShapeTool, + MycrozineTemplateTool, + MarkdownTool ] +const updatedOverrides = { + ...overrides, + actions(editor: Editor, actions: any) { + return { + ...actions, + 'next-slide': { + id: 'next-slide', + label: 'Next slide', + kbd: 'right', + onSelect() { + const slides = editor.getCurrentPageShapes().filter(shape => shape.type === 'Slide') + if (slides.length === 0) return + + const currentSlide = editor.getSelectedShapes().find(shape => shape.type === 'Slide') + const currentIndex = currentSlide + ? slides.findIndex(slide => slide.id === currentSlide.id) + : -1 + +console.log('Current index:', currentIndex) +console.log('Current slide:', currentSlide) + + + // Calculate next index with wraparound + const nextIndex = currentIndex === -1 + ? 0 + : currentIndex >= slides.length - 1 + ? 0 + : currentIndex + 1 + + const nextSlide = slides[nextIndex] + + editor.select(nextSlide.id) + editor.stopCameraAnimation() + moveToSlide(editor, nextSlide as ISlideShape) + }, + }, + 'previous-slide': { + id: 'previous-slide', + label: 'Previous slide', + kbd: 'left', + onSelect() { + const slides = editor.getCurrentPageShapes().filter(shape => shape.type === 'Slide') + if (slides.length === 0) return + + const currentSlide = editor.getSelectedShapes().find(shape => shape.type === 'Slide') + const currentIndex = currentSlide + ? slides.findIndex(slide => slide.id === currentSlide.id) + : -1 + + // Calculate previous index with wraparound + const previousIndex = currentIndex <= 0 + ? slides.length - 1 + : currentIndex - 1 + + const previousSlide = slides[previousIndex] + + editor.select(previousSlide.id) + editor.stopCameraAnimation() + moveToSlide(editor, previousSlide as ISlideShape) + }, + }, + } + }, +} + export function Board() { const { slug } = useParams<{ slug: string }>() const roomId = slug || "default-room" @@ -47,7 +144,7 @@ export function Board() { () => ({ uri: `${WORKER_URL}/connect/${roomId}`, assets: multiplayerAssetStore, - shapeUtils: [...shapeUtils, ...defaultShapeUtils], + shapeUtils: [...defaultShapeUtils, ...customShapeUtils], bindingUtils: [...defaultBindingUtils], }), [roomId], @@ -56,14 +153,17 @@ export function Board() { const store = useSync(storeConfig) const [editor, setEditor] = useState(null) + //console.log("store:", store) + //console.log("store.store:",store.store) + return (
+ +export class SlideShapeUtil extends BaseBoxShapeUtil { + static override type = "Slide" + + // static override props = { + // w: T.number, + // h: T.number, + // } + + override canBind = () => false + override hideRotateHandle = () => true + + getDefaultProps(): ISlideShape["props"] { + return { + w: 720, + h: 480, + } + } + + getGeometry(shape: ISlideShape): Geometry2d { + return new Rectangle2d({ + width: shape.props.w, + height: shape.props.h, + isFilled: false, + }) + } + + override onRotate = (initial: ISlideShape) => initial + override onResize(shape: ISlideShape, info: any) { + return resizeBox(shape, info) + } + + override onDoubleClick = (shape: ISlideShape) => { + moveToSlide(this.editor, shape) + this.editor.selectNone() + } + + override onDoubleClickEdge = (shape: ISlideShape) => { + moveToSlide(this.editor, shape) + this.editor.selectNone() + } + + component(shape: ISlideShape) { + const bounds = this.editor.getShapeGeometry(shape).bounds + + // eslint-disable-next-line react-hooks/rules-of-hooks + const zoomLevel = useValue('zoom level', () => this.editor.getZoomLevel(), [this.editor]) + + // eslint-disable-next-line react-hooks/rules-of-hooks + const slides = useSlides() + const index = slides.findIndex((s) => s.id === shape.id) + + // eslint-disable-next-line react-hooks/rules-of-hooks + const handleLabelPointerDown = useCallback(() => this.editor.select(shape.id), [shape.id]) + + if (!bounds) return null + + return ( + <> +
+ {`Slide ${index + 1}`} +
+ + + {bounds.sides.map((side, i) => { + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + side[0].dist(side[1]), + 1 / zoomLevel, + { + style: 'dashed', + lengthRatio: 6, + } + ) + + return ( + + ) + })} + + + + ) + } + + indicator(shape: ISlideShape) { + return + } +} \ No newline at end of file diff --git a/src/slides/SlidesPanel.tsx b/src/slides/SlidesPanel.tsx new file mode 100644 index 0000000..d3de3d6 --- /dev/null +++ b/src/slides/SlidesPanel.tsx @@ -0,0 +1,37 @@ +import { TldrawUiButton, stopEventPropagation, track, useEditor, useValue } from 'tldraw' +import { moveToSlide, useCurrentSlide, useSlides } from '@/slides/useSlides' + +export const SlidesPanel = track(() => { + const editor = useEditor() + const slides = useSlides() + const currentSlide = useCurrentSlide() + const selectedShapes = useValue('selected shapes', () => editor.getSelectedShapes(), [editor]) + + if (slides.length === 0) return null + return ( +
stopEventPropagation(e)}> + {slides.map((slide, i) => { + const isSelected = selectedShapes.includes(slide) + return ( + { + moveToSlide(editor, slide) + // Switch to select tool and select the slide shape + editor.setCurrentTool('select') + editor.select(slide) + }} + style={{ + background: currentSlide?.id === slide.id ? 'var(--color-background)' : 'transparent', + outline: isSelected ? 'var(--color-selection-stroke) solid 1.5px' : 'none', + }} + > + {`Slide ${i + 1}`} + + ) + })} +
+ ) +}) \ No newline at end of file diff --git a/src/slides/slides.css b/src/slides/slides.css new file mode 100644 index 0000000..b5144ad --- /dev/null +++ b/src/slides/slides.css @@ -0,0 +1,32 @@ +.slides-panel { + display: flex; + flex-direction: column; + gap: 4px; + max-height: calc(100% - 110px); + margin: 50px 0px; + padding: 4px; + background-color: var(--color-low); + pointer-events: all; + border-top-right-radius: var(--radius-4); + border-bottom-right-radius: var(--radius-4); + overflow: auto; + border-right: 2px solid var(--color-background); + border-bottom: 2px solid var(--color-background); + border-top: 2px solid var(--color-background); +} + +.slides-panel-button { + border-radius: var(--radius-4); + outline-offset: -1px; +} + +.slide-shape-label { + pointer-events: all; + position: absolute; + background: var(--color-low); + padding: calc(12px * var(--tl-scale)); + border-bottom-right-radius: calc(var(--radius-4) * var(--tl-scale)); + font-size: calc(12px * var(--tl-scale)); + color: var(--color-text); + white-space: nowrap; +} \ No newline at end of file diff --git a/src/slides/useSlides.tsx b/src/slides/useSlides.tsx new file mode 100644 index 0000000..d743d8a --- /dev/null +++ b/src/slides/useSlides.tsx @@ -0,0 +1,31 @@ +import { EASINGS, Editor, atom, useEditor, useValue } from 'tldraw' +import { ISlideShape } from '@/shapes/SlideShapeUtil' + +export const $currentSlide = atom('current slide', null) + +export function moveToSlide(editor: Editor, slide: ISlideShape) { + const bounds = editor.getShapePageBounds(slide.id) + if (!bounds) return + $currentSlide.set(slide) + editor.selectNone() + editor.zoomToBounds(bounds, { + animation: { duration: 500, easing: EASINGS.easeInOutCubic }, + inset: 0, + }) +} + +export function useSlides() { + const editor = useEditor() + return useValue('slide shapes', () => getSlides(editor), [editor]) +} + +export function useCurrentSlide() { + return useValue($currentSlide) +} + +export function getSlides(editor: Editor) { + return editor + .getSortedChildIdsForParent(editor.getCurrentPageId()) + .map((id) => editor.getShape(id)) + .filter((s) => s?.type === 'Slide') as ISlideShape[] +} diff --git a/src/tools/SlideShapeTool.ts b/src/tools/SlideShapeTool.ts new file mode 100644 index 0000000..87746b7 --- /dev/null +++ b/src/tools/SlideShapeTool.ts @@ -0,0 +1,12 @@ +import { BaseBoxShapeTool } from 'tldraw' + +export class SlideShapeTool extends BaseBoxShapeTool { + static override id = 'Slide' + static override initial = 'idle' + override shapeType = 'Slide' + + constructor(editor: any) { + super(editor) + console.log('SlideShapeTool constructed', { id: this.id, shapeType: this.shapeType }) + } +} \ No newline at end of file diff --git a/src/ui/CustomContextMenu.tsx b/src/ui/CustomContextMenu.tsx index c7ee1b2..dabf26d 100644 --- a/src/ui/CustomContextMenu.tsx +++ b/src/ui/CustomContextMenu.tsx @@ -102,7 +102,7 @@ export function CustomContextMenu(props: TLUiContextMenuProps) { {/* Creation Tools Group */} - {/* + { + editor.setCurrentTool("Slide") + }} + /> { editor.setCurrentTool("Markdown") }} - /> - */} + /> {/* Frame Controls */} diff --git a/src/ui/CustomToolbar.tsx b/src/ui/CustomToolbar.tsx index 1d3ea15..1fc13ba 100644 --- a/src/ui/CustomToolbar.tsx +++ b/src/ui/CustomToolbar.tsx @@ -44,6 +44,14 @@ export function CustomToolbar() { isSelected={tools["Embed"].id === editor.getCurrentToolId()} /> )} + {tools["SlideShape"] && ( + + )} {/* {tools["Markdown"] && ( makeRealSettings.get(), []) + + useReactor( + 'update settings local storage', + () => { + localStorage.setItem('makereal_settings_2', JSON.stringify(makeRealSettings.get())) + }, + [] + ) + + return ( + <> + + Settings + + + +

+ To use Make Real, enter your API key for each model provider that you wish to use. Draw + some shapes, then select the shapes and click Make Real.{' '} + + Read our guide. + +

+
+
+ +
+ + {settings.provider !== 'all' && ( + <> +
+ +
+ + + )} +
+
+ {PROVIDERS.map((provider) => { + if (provider.id === 'google') return null + const value = settings.keys[provider.id as keyof typeof settings.keys] + return ( + + ) + })} + {/*
+
+ +
+ { + const next = { ...settings, keys: { ...settings.keys, google: value } } + localStorage.setItem('makereal_settings_2', JSON.stringify(next)) + makeRealSettings.set(next) + }} + /> +
*/} +
+
+
+ + +
+ { + makeRealSettings.update((s) => ({ ...s, prompts: { ...s.prompts, system: value } })) + }} + /> +
+
+ + { + onClose() + }} + > + Save + + + + ) +} + +function ApiKeyInput({ + provider, + value, + warning, +}: { + provider: (typeof PROVIDERS)[number] + value: string + warning: boolean +}) { + const isValid = value.length === 0 || provider.validate(value) + + return ( +
+
+ + + + +
+ { + makeRealSettings.update((s) => ({ ...s, keys: { ...s.keys, [provider.id]: value } })) + }} + /> +
+ ) +} \ No newline at end of file diff --git a/src/ui/overrides.tsx b/src/ui/overrides.tsx index 944871e..64b93b7 100644 --- a/src/ui/overrides.tsx +++ b/src/ui/overrides.tsx @@ -1,4 +1,4 @@ -import { TLUiOverrides } from "tldraw" +import { shapeIdValidator, TLUiOverrides } from "tldraw" import { cameraHistory, copyLinkToCurrentView, @@ -68,6 +68,7 @@ export const overrides: TLUiOverrides = { label: "Video Chat", kbd: "alt+v", readonlyOk: true, + type: "VideoChat", onSelect: () => editor.setCurrentTool("VideoChat"), }, ChatBox: { @@ -76,6 +77,7 @@ export const overrides: TLUiOverrides = { label: "Chat", kbd: "alt+c", readonlyOk: true, + type: "ChatBox", onSelect: () => editor.setCurrentTool("ChatBox"), }, Embed: { @@ -84,26 +86,41 @@ export const overrides: TLUiOverrides = { label: "Embed", kbd: "alt+e", readonlyOk: true, + type: "Embed", onSelect: () => editor.setCurrentTool("Embed"), }, - /* + SlideShape: { + id: "Slide", + icon: "slides", + label: "Slide", + kbd: "alt+s", + type: "Slide", + readonlyOk: true, + onSelect: () => { + console.log('SlideShape tool selected from menu') + console.log('Current tool before:', editor.getCurrentToolId()) + editor.setCurrentTool("Slide") + console.log('Current tool after:', editor.getCurrentToolId()) + }, + }, Markdown: { id: "Markdown", icon: "markdown", label: "Markdown", kbd: "alt+m", readonlyOk: true, + type: "Markdown", onSelect: () => editor.setCurrentTool("Markdown"), }, MycrozineTemplate: { id: "MycrozineTemplate", icon: "rectangle", label: "Mycrozine Template", + type: "MycrozineTemplate", kbd: "m", readonlyOk: true, onSelect: () => editor.setCurrentTool("MycrozineTemplate"), }, - */ hand: { ...tools.hand, onDoubleClick: (info: any) => { diff --git a/worker/TldrawDurableObject.ts b/worker/TldrawDurableObject.ts index d2bb3dd..927d31f 100644 --- a/worker/TldrawDurableObject.ts +++ b/worker/TldrawDurableObject.ts @@ -7,6 +7,7 @@ import { createTLSchema, defaultBindingSchemas, defaultShapeSchemas, + shapeIdValidator, } from "@tldraw/tlschema" import { AutoRouter, IRequest, error } from "itty-router" import throttle from "lodash.throttle" @@ -16,6 +17,8 @@ import { VideoChatShape } from "@/shapes/VideoChatShapeUtil" import { EmbedShape } from "@/shapes/EmbedShapeUtil" import { MarkdownShape } from "@/shapes/MarkdownShapeUtil" import { MycrozineTemplateShape } from "@/shapes/MycrozineTemplateShapeUtil" +import { T } from "@tldraw/tldraw" +import { SlideShapeUtil } from "@/shapes/SlideShapeUtil" // add custom shapes and bindings here if needed: export const customSchema = createTLSchema({ @@ -41,6 +44,10 @@ export const customSchema = createTLSchema({ props: MycrozineTemplateShape.props, migrations: MycrozineTemplateShape.migrations, }, + Slide: { + props: SlideShapeUtil.props, + migrations: SlideShapeUtil.migrations, + }, }, bindings: defaultBindingSchemas, }) @@ -206,7 +213,7 @@ export class TldrawDurableObject { initialSnapshot.documents = initialSnapshot.documents.filter( (record) => { const shape = record.state as TLShape - return shape.type !== "chatBox" + return shape.type !== "ChatBox" }, ) }