slidedeck shape installed, still minor difficulty in keyboard arrow transition between slides (last slide + wraparound)

This commit is contained in:
Jeff-Emmett 2025-01-23 14:14:04 +01:00
parent 2590a86352
commit a0e73b0f9e
15 changed files with 1086 additions and 23 deletions

399
package-lock.json generated
View File

@ -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"
}
}
}
}

View File

@ -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",

61
src/lib/settings.tsx Normal file
View File

@ -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
}

24
src/prompt.ts Normal file
View File

@ -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."

View File

@ -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 (
<DefaultToolbar {...props}>
{slideTool && <TldrawUiMenuItem {...slideTool} isSelected={isSlideSelected} />}
<DefaultToolbarContent />
</DefaultToolbar>
)
},
KeyboardShortcutsDialog: (props: any) => {
const tools = useTools()
return (
<DefaultKeyboardShortcutsDialog {...props}>
<TldrawUiMenuItem {...tools['Slide']} />
<DefaultKeyboardShortcutsDialogContent />
</DefaultKeyboardShortcutsDialog>
)
},
}
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<Editor | null>(null)
//console.log("store:", store)
//console.log("store.store:",store.store)
return (
<div style={{ position: "fixed", inset: 0 }}>
<Tldraw
store={store.store}
shapeUtils={shapeUtils}
tools={tools}
components={components}
overrides={overrides}
shapeUtils={customShapeUtils}
tools={customTools}
components={updatedComponents}
overrides={updatedOverrides}
cameraOptions={{
zoomSteps: [
0.001, // Min zoom

View File

@ -0,0 +1,128 @@
import { useCallback } from 'react'
import {
BaseBoxShapeUtil,
Geometry2d,
RecordProps,
Rectangle2d,
SVGContainer,
ShapeUtil,
T,
TLBaseShape,
getPerfectDashProps,
resizeBox,
useValue,
} from 'tldraw'
import { moveToSlide, useSlides } from '@/slides/useSlides'
export type ISlideShape = TLBaseShape<
'Slide',
{
w: number
h: number
}
>
export class SlideShapeUtil extends BaseBoxShapeUtil<ISlideShape> {
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 (
<>
<div onPointerDown={handleLabelPointerDown} className="slide-shape-label">
{`Slide ${index + 1}`}
</div>
<SVGContainer>
<g
style={{
stroke: 'var(--color-text)',
strokeWidth: 'calc(1px * var(--tl-scale))',
opacity: 0.25,
}}
pointerEvents="none"
strokeLinecap="round"
strokeLinejoin="round"
>
{bounds.sides.map((side, i) => {
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
side[0].dist(side[1]),
1 / zoomLevel,
{
style: 'dashed',
lengthRatio: 6,
}
)
return (
<line
key={i}
x1={side[0].x}
y1={side[0].y}
x2={side[1].x}
y2={side[1].y}
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
/>
)
})}
</g>
</SVGContainer>
</>
)
}
indicator(shape: ISlideShape) {
return <rect width={shape.props.w} height={shape.props.h} />
}
}

View File

@ -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 (
<div className="slides-panel scroll-light" onPointerDown={(e) => stopEventPropagation(e)}>
{slides.map((slide, i) => {
const isSelected = selectedShapes.includes(slide)
return (
<TldrawUiButton
key={'slides-panel-button:' + slide.id}
type="normal"
className="slides-panel-button"
onClick={() => {
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}`}
</TldrawUiButton>
)
})}
</div>
)
})

32
src/slides/slides.css Normal file
View File

@ -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;
}

31
src/slides/useSlides.tsx Normal file
View File

@ -0,0 +1,31 @@
import { EASINGS, Editor, atom, useEditor, useValue } from 'tldraw'
import { ISlideShape } from '@/shapes/SlideShapeUtil'
export const $currentSlide = atom<ISlideShape | null>('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<ISlideShape[]>('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[]
}

View File

@ -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 })
}
}

View File

@ -102,7 +102,7 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
{/* Creation Tools Group */}
<TldrawUiMenuGroup id="creation-tools">
<TldrawUiMenuItem
id="video-chat"
id="VideoChat"
label="Create Video Chat"
icon="video"
kbd="alt+v"
@ -112,7 +112,7 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
}}
/>
<TldrawUiMenuItem
id="chat-box"
id="ChatBox"
label="Create Chat Box"
icon="chat"
kbd="alt+c"
@ -122,7 +122,7 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
}}
/>
<TldrawUiMenuItem
id="embed"
id="Embed"
label="Create Embed"
icon="embed"
kbd="alt+e"
@ -131,7 +131,16 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
editor.setCurrentTool("Embed")
}}
/>
{/*
<TldrawUiMenuItem
id="Slide"
label="Create Slide"
icon="slides"
kbd="alt+s"
disabled={hasSelection}
onSelect={() => {
editor.setCurrentTool("Slide")
}}
/>
<TldrawUiMenuItem
id="mycrozine-template"
label="Create Mycrozine Template"
@ -151,8 +160,7 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
onSelect={() => {
editor.setCurrentTool("Markdown")
}}
/>
*/}
/>
</TldrawUiMenuGroup>
{/* Frame Controls */}

View File

@ -44,6 +44,14 @@ export function CustomToolbar() {
isSelected={tools["Embed"].id === editor.getCurrentToolId()}
/>
)}
{tools["SlideShape"] && (
<TldrawUiMenuItem
{...tools["SlideShape"]}
icon="slides"
label="Slide"
isSelected={tools["SlideShape"].id === editor.getCurrentToolId()}
/>
)}
{/*
{tools["Markdown"] && (
<TldrawUiMenuItem

198
src/ui/SettingsDialog.tsx Normal file
View File

@ -0,0 +1,198 @@
import {
TLUiDialogProps,
TldrawUiButton,
TldrawUiButtonLabel,
TldrawUiDialogBody,
TldrawUiDialogCloseButton,
TldrawUiDialogFooter,
TldrawUiDialogHeader,
TldrawUiDialogTitle,
TldrawUiIcon,
TldrawUiInput,
useReactor,
useValue,
} from 'tldraw'
import { PROVIDERS, makeRealSettings } from '../lib/settings'
import { SYSTEM_PROMPT } from '@/prompt'
export function SettingsDialog({ onClose }: TLUiDialogProps) {
const settings = useValue('settings', () => makeRealSettings.get(), [])
useReactor(
'update settings local storage',
() => {
localStorage.setItem('makereal_settings_2', JSON.stringify(makeRealSettings.get()))
},
[]
)
return (
<>
<TldrawUiDialogHeader>
<TldrawUiDialogTitle>Settings</TldrawUiDialogTitle>
<TldrawUiDialogCloseButton />
</TldrawUiDialogHeader>
<TldrawUiDialogBody
style={{ maxWidth: 350, display: 'flex', flexDirection: 'column', gap: 8 }}
>
<p>
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.{' '}
<a
target="_blank"
href="https://tldraw.notion.site/Make-Real-FAQs-93be8b5273d14f7386e14eb142575e6e?pvs=4"
>
<u>Read our guide.</u>
</a>
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, marginTop: 8 }}>
<div style={{ display: 'flex', flexDirection: 'row', gap: 4 }}>
<label style={{ flexGrow: 2 }}>Provider</label>
</div>
<select
className="apikey_select"
value={settings.provider}
onChange={(e) => {
makeRealSettings.update((s) => ({ ...s, provider: e.target.value as any }))
}}
>
{PROVIDERS.map((provider) => {
return (
<option key={provider.id + 'option'} value={provider.id}>
{provider.name}
</option>
)
})}
<option value="all">All</option>
</select>
{settings.provider !== 'all' && (
<>
<div style={{ display: 'flex', flexDirection: 'row', gap: 4 }}>
<label style={{ flexGrow: 2 }}>Model</label>
</div>
<select
className="apikey_select"
value={settings.models[settings.provider]}
onChange={(e) => {
makeRealSettings.update((s) => ({
...s,
models: { ...s.models, [settings.provider]: e.target.value as any },
}))
}}
>
{PROVIDERS.find((p) => p.id === settings.provider)!.models.map((model) => {
return (
<option key={model + 'option'} value={model}>
{model}
</option>
)
})}
</select>
</>
)}
</div>
<hr style={{ margin: '12px 0px' }} />
{PROVIDERS.map((provider) => {
if (provider.id === 'google') return null
const value = settings.keys[provider.id as keyof typeof settings.keys]
return (
<ApiKeyInput
provider={provider}
key={provider.name + 'key'}
value={value}
warning={
value === '' && (settings.provider === provider.id || settings.provider === 'all')
}
/>
)
})}
{/* <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<div style={{ display: 'flex', flexDirection: 'row', gap: 4 }}>
<label style={{ flexGrow: 2 }}>Google</label>
</div>
<TldrawUiInput
className="apikey_input"
value={settings.keys.google}
placeholder="risky but cool"
onValueChange={(value) => {
const next = { ...settings, keys: { ...settings.keys, google: value } }
localStorage.setItem('makereal_settings_2', JSON.stringify(next))
makeRealSettings.set(next)
}}
/>
</div> */}
<hr style={{ margin: '12px 0px' }} />
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<div style={{ display: 'flex', flexDirection: 'row', gap: 4 }}>
<label style={{ flexGrow: 2 }}>System prompt</label>
<button
style={{ all: 'unset', cursor: 'pointer' }}
onClick={() => {
makeRealSettings.update((s) => ({
...s,
prompts: { ...s.prompts, system: SYSTEM_PROMPT },
}))
}}
>
Reset
</button>
</div>
<TldrawUiInput
className="apikey_input"
value={settings.prompts.system}
onValueChange={(value) => {
makeRealSettings.update((s) => ({ ...s, prompts: { ...s.prompts, system: value } }))
}}
/>
</div>
</TldrawUiDialogBody>
<TldrawUiDialogFooter className="tlui-dialog__footer__actions">
<TldrawUiButton
type="primary"
onClick={async () => {
onClose()
}}
>
<TldrawUiButtonLabel>Save</TldrawUiButtonLabel>
</TldrawUiButton>
</TldrawUiDialogFooter>
</>
)
}
function ApiKeyInput({
provider,
value,
warning,
}: {
provider: (typeof PROVIDERS)[number]
value: string
warning: boolean
}) {
const isValid = value.length === 0 || provider.validate(value)
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<div style={{ display: 'flex', flexDirection: 'row', gap: 4, alignItems: 'center' }}>
<label style={{ flexGrow: 2, color: warning ? 'red' : 'var(--color-text)' }}>
{provider.name} API key
</label>
<a style={{ cursor: 'pointer', pointerEvents: 'all' }} target="_blank" href={provider.help}>
<TldrawUiIcon
className="apikey_help_icon"
small
icon={provider.validate(value) ? 'check' : 'question-mark-circle'}
/>
</a>
</div>
<TldrawUiInput
className={`apikey_input ${isValid ? '' : 'apikey_input__invalid'}`}
value={value}
placeholder="risky but cool"
onValueChange={(value) => {
makeRealSettings.update((s) => ({ ...s, keys: { ...s.keys, [provider.id]: value } }))
}}
/>
</div>
)
}

View File

@ -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) => {

View File

@ -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"
},
)
}