automerge beginnings

This commit is contained in:
Jeff Emmett 2025-07-29 17:34:18 -04:00
parent 5eb5789c23
commit de7de98ee8
7 changed files with 857 additions and 1 deletions

291
package-lock.json generated
View File

@ -10,6 +10,9 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.33.1", "@anthropic-ai/sdk": "^0.33.1",
"@automerge/automerge": "^3.1.1",
"@automerge/automerge-repo": "^2.2.0",
"@automerge/automerge-repo-react-hooks": "^2.2.0",
"@daily-co/daily-js": "^0.60.0", "@daily-co/daily-js": "^0.60.0",
"@daily-co/daily-react": "^0.20.0", "@daily-co/daily-react": "^0.20.0",
"@tldraw/assets": "^3.6.0", "@tldraw/assets": "^3.6.0",
@ -22,6 +25,7 @@
"@uiw/react-md-editor": "^4.0.5", "@uiw/react-md-editor": "^4.0.5",
"@vercel/analytics": "^1.2.2", "@vercel/analytics": "^1.2.2",
"ai": "^4.1.0", "ai": "^4.1.0",
"automerge": "^0.14.2",
"cherry-markdown": "^0.8.57", "cherry-markdown": "^0.8.57",
"cloudflare-workers-unfurl": "^0.0.7", "cloudflare-workers-unfurl": "^0.0.7",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
@ -182,6 +186,56 @@
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
}, },
"node_modules/@automerge/automerge": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@automerge/automerge/-/automerge-3.1.1.tgz",
"integrity": "sha512-x7tZiMBLk4/SKYimVEVl1/wPntT9buGvLOWCey9ZcH8JUsB0dgm49C0S7Ojzgvflcs2hc/YjiXRPcFeFkinIgw==",
"license": "MIT"
},
"node_modules/@automerge/automerge-repo": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@automerge/automerge-repo/-/automerge-repo-2.2.0.tgz",
"integrity": "sha512-/4cAxnUDPykqdSMJJiJvPlR2VY0JHJnvEoM7fO8qVXwQors34S0VkJEScXRJZExcVhWGhFTwCTWvUbb6hhWQIQ==",
"license": "MIT",
"dependencies": {
"@automerge/automerge": "2.2.8 - 3",
"bs58check": "^3.0.1",
"cbor-x": "^1.3.0",
"debug": "^4.3.4",
"eventemitter3": "^5.0.1",
"fast-sha256": "^1.3.0",
"uuid": "^9.0.0",
"xstate": "^5.9.1"
}
},
"node_modules/@automerge/automerge-repo-react-hooks": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@automerge/automerge-repo-react-hooks/-/automerge-repo-react-hooks-2.2.0.tgz",
"integrity": "sha512-zHP44jXSCmV1DyyKdTKgv8nz7yrdCT7VFXs/QrF3YRhE/3czhhvbXo2zahGvOoM+jryH0eJxHTrJ7jMEtt/Ung==",
"license": "MIT",
"dependencies": {
"@automerge/automerge": "2.2.8 - 3",
"@automerge/automerge-repo": "2.2.0",
"eventemitter3": "^5.0.1",
"react-usestateref": "^1.0.8"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
}
},
"node_modules/@automerge/automerge-repo-react-hooks/node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
"node_modules/@automerge/automerge-repo/node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.26.2", "version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
@ -473,6 +527,84 @@
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
"node_modules/@cbor-extract/cbor-extract-darwin-arm64": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz",
"integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@cbor-extract/cbor-extract-darwin-x64": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz",
"integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@cbor-extract/cbor-extract-linux-arm": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz",
"integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@cbor-extract/cbor-extract-linux-arm64": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz",
"integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@cbor-extract/cbor-extract-linux-x64": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz",
"integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@cbor-extract/cbor-extract-win32-x64": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz",
"integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@cloudflare/intl-types": { "node_modules/@cloudflare/intl-types": {
"version": "1.5.6", "version": "1.5.6",
"resolved": "https://registry.npmjs.org/@cloudflare/intl-types/-/intl-types-1.5.6.tgz", "resolved": "https://registry.npmjs.org/@cloudflare/intl-types/-/intl-types-1.5.6.tgz",
@ -1838,6 +1970,18 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -4384,6 +4528,28 @@
"node": ">= 4.5.0" "node": ">= 4.5.0"
} }
}, },
"node_modules/automerge": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/automerge/-/automerge-0.14.2.tgz",
"integrity": "sha512-shiwuJHCbNRI23WZyIECLV4Ovf3WiAFJ7P9BH4l5gON1In/UUbjcSJKRygtIirObw2UQumeYxp3F2XBdSvQHnA==",
"license": "MIT",
"dependencies": {
"immutable": "^3.8.2",
"transit-immutable-js": "^0.7.0",
"transit-js": "^0.8.861",
"uuid": "^3.4.0"
}
},
"node_modules/automerge/node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"license": "MIT",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/bail": { "node_modules/bail": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
@ -4400,6 +4566,12 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/base-x": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz",
"integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==",
"license": "MIT"
},
"node_modules/base64-arraybuffer": { "node_modules/base64-arraybuffer": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
@ -4508,6 +4680,25 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/bs58": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
"integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
"license": "MIT",
"dependencies": {
"base-x": "^4.0.0"
}
},
"node_modules/bs58check": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz",
"integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.2.0",
"bs58": "^5.0.0"
}
},
"node_modules/btoa": { "node_modules/btoa": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
@ -4608,6 +4799,37 @@
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
"node_modules/cbor-extract": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.0.tgz",
"integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-gyp-build-optional-packages": "5.1.1"
},
"bin": {
"download-cbor-prebuilds": "bin/download-prebuilds.js"
},
"optionalDependencies": {
"@cbor-extract/cbor-extract-darwin-arm64": "2.2.0",
"@cbor-extract/cbor-extract-darwin-x64": "2.2.0",
"@cbor-extract/cbor-extract-linux-arm": "2.2.0",
"@cbor-extract/cbor-extract-linux-arm64": "2.2.0",
"@cbor-extract/cbor-extract-linux-x64": "2.2.0",
"@cbor-extract/cbor-extract-win32-x64": "2.2.0"
}
},
"node_modules/cbor-x": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.6.0.tgz",
"integrity": "sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg==",
"license": "MIT",
"optionalDependencies": {
"cbor-extract": "^2.2.0"
}
},
"node_modules/ccount": { "node_modules/ccount": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
@ -6530,6 +6752,12 @@
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
"node_modules/fast-sha256": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
"license": "Unlicense"
},
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.18.0", "version": "1.18.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
@ -7390,6 +7618,15 @@
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/immutable": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
"integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inflight": { "node_modules/inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -9155,6 +9392,21 @@
"node-gyp-build-test": "build-test.js" "node-gyp-build-test": "build-test.js"
} }
}, },
"node_modules/node-gyp-build-optional-packages": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz",
"integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==",
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^2.0.1"
},
"bin": {
"node-gyp-build-optional-packages": "bin.js",
"node-gyp-build-optional-packages-optional": "optional.js",
"node-gyp-build-optional-packages-test": "build-test.js"
}
},
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.19", "version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
@ -9866,6 +10118,15 @@
} }
} }
}, },
"node_modules/react-usestateref": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/react-usestateref/-/react-usestateref-1.0.9.tgz",
"integrity": "sha512-t8KLsI7oje0HzfzGhxFXzuwbf1z9vhBM1ptHLUIHhYqZDKFuI5tzdhEVxSNzUkYxwF8XdpOErzHlKxvP7sTERw==",
"license": "ISC",
"peerDependencies": {
"react": ">16.0.0"
}
},
"node_modules/readable-stream": { "node_modules/readable-stream": {
"version": "3.6.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@ -11097,6 +11358,25 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/transit-immutable-js": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/transit-immutable-js/-/transit-immutable-js-0.7.0.tgz",
"integrity": "sha512-Qga6EDAmVVoDwzPZ2fBvi3aj5E0V6NLy/spCFvspVd4BfY2/3Jr5yyynbSTC2SAEipkJ4MHVH75xNcTOlz9UaQ==",
"license": "MIT",
"peerDependencies": {
"immutable": ">= 3",
"transit-js": ">= 0.8"
}
},
"node_modules/transit-js": {
"version": "0.8.874",
"resolved": "https://registry.npmjs.org/transit-js/-/transit-js-0.8.874.tgz",
"integrity": "sha512-IDJJGKRzUbJHmN0P15HBBa05nbKor3r2MmG6aSt0UxXIlJZZKcddTk67/U7WyAeW9Hv/VYI02IqLzolsC4sbPA==",
"license": "Apache-2.0",
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/tree-kill": { "node_modules/tree-kill": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@ -11526,7 +11806,6 @@
"https://github.com/sponsors/ctavan" "https://github.com/sponsors/ctavan"
], ],
"license": "MIT", "license": "MIT",
"optional": true,
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "dist/bin/uuid"
} }
@ -12457,6 +12736,16 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/xstate": {
"version": "5.20.1",
"resolved": "https://registry.npmjs.org/xstate/-/xstate-5.20.1.tgz",
"integrity": "sha512-i9ZpNnm/XhCOMUxae1suT8PjYNTStZWbhmuKt4xeTPaYG5TS0Fz0i+Ka5yxoNPpaHW3VW6JIowrwFgSTZONxig==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/xstate"
}
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -17,6 +17,9 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.33.1", "@anthropic-ai/sdk": "^0.33.1",
"@automerge/automerge": "^3.1.1",
"@automerge/automerge-repo": "^2.2.0",
"@automerge/automerge-repo-react-hooks": "^2.2.0",
"@daily-co/daily-js": "^0.60.0", "@daily-co/daily-js": "^0.60.0",
"@daily-co/daily-react": "^0.20.0", "@daily-co/daily-react": "^0.20.0",
"@tldraw/assets": "^3.6.0", "@tldraw/assets": "^3.6.0",
@ -29,6 +32,7 @@
"@uiw/react-md-editor": "^4.0.5", "@uiw/react-md-editor": "^4.0.5",
"@vercel/analytics": "^1.2.2", "@vercel/analytics": "^1.2.2",
"ai": "^4.1.0", "ai": "^4.1.0",
"automerge": "^0.14.2",
"cherry-markdown": "^0.8.57", "cherry-markdown": "^0.8.57",
"cloudflare-workers-unfurl": "^0.0.7", "cloudflare-workers-unfurl": "^0.0.7",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",

121
src/AutomergeToTLStore.ts Normal file
View File

@ -0,0 +1,121 @@
import { TLRecord, RecordId, TLStore } from "@tldraw/tldraw"
import { Patch } from "@automerge/automerge"
export function applyAutomergePatchesToTLStore(
patches: Patch[],
store: TLStore
) {
const toRemove: TLRecord["id"][] = []
const updatedObjects: { [id: string]: TLRecord } = {}
patches.forEach((patch) => {
if (!isStorePatch(patch)) return
const id = pathToId(patch.path)
const record =
updatedObjects[id] || JSON.parse(JSON.stringify(store.get(id) || {}))
switch (patch.action) {
case "insert": {
updatedObjects[id] = applyInsertToObject(patch, record)
break
}
case "put":
updatedObjects[id] = applyPutToObject(patch, record)
break
case "splice": {
updatedObjects[id] = applySpliceToObject(patch, record)
break
}
case "del": {
const id = pathToId(patch.path)
toRemove.push(id as TLRecord["id"])
break
}
default: {
console.log("Unsupported patch:", patch)
}
}
})
const toPut = Object.values(updatedObjects)
// put / remove the records in the store
console.log({ patches, toPut })
store.mergeRemoteChanges(() => {
if (toRemove.length) store.remove(toRemove)
if (toPut.length) store.put(toPut)
})
}
const isStorePatch = (patch: Patch): boolean => {
return patch.path[0] === "store" && patch.path.length > 1
}
// path: ["store", "camera:page:page", "x"] => "camera:page:page"
const pathToId = (path: (string | number)[]): RecordId<any> => {
return path[1] as RecordId<any>
}
const applyInsertToObject = (patch: any, object: any): TLRecord => {
const { path, values } = patch
let current = object
const insertionPoint = path[path.length - 1]
const pathEnd = path[path.length - 2]
const parts = path.slice(2, -2)
for (const part of parts) {
if (current[part] === undefined) {
throw new Error("NO WAY")
}
current = current[part]
}
// splice is a mutator... yay.
const clone = current[pathEnd].slice(0)
clone.splice(insertionPoint, 0, ...values)
current[pathEnd] = clone
return object
}
const applyPutToObject = (patch: any, object: any): TLRecord => {
const { path, value } = patch
let current = object
// special case
if (path.length === 2) {
// this would be creating the object, but we have done
return object
}
const parts = path.slice(2, -2)
const property = path[path.length - 1]
const target = path[path.length - 2]
if (path.length === 3) {
return { ...object, [property]: value }
}
// default case
for (const part of parts) {
current = current[part]
}
current[target] = { ...current[target], [property]: value }
return object
}
const applySpliceToObject = (patch: any, object: any): TLRecord => {
const { path, value } = patch
let current = object
const insertionPoint = path[path.length - 1]
const pathEnd = path[path.length - 2]
const parts = path.slice(2, -2)
for (const part of parts) {
if (current[part] === undefined) {
throw new Error("NO WAY")
}
current = current[part]
}
// TODO: we're not supporting actual splices yet because TLDraw won't generate them natively
if (insertionPoint !== 0) {
throw new Error("Splices are not supported yet")
}
current[pathEnd] = value // .splice(insertionPoint, 0, value)
return object
}

View File

@ -0,0 +1,49 @@
import { type DocHandle } from "@automerge/automerge-repo"
import { type TLStoreSnapshot, Tldraw, track, useEditor } from "@tldraw/tldraw"
import "@tldraw/tldraw/tldraw.css"
import { useAutomergeStore } from "./useAutomergeStore"
interface TLDrawAutomergeExampleProps {
handle: DocHandle<TLStoreSnapshot>
userId: string
}
export function TLDrawAutomergeExample({
handle,
userId,
}: TLDrawAutomergeExampleProps) {
const store = useAutomergeStore({ handle, userId })
return (
<div className="tldraw__editor">
<Tldraw autoFocus store={store} />
</div>
)
}
const NameEditor = track(() => {
const editor = useEditor()
return (
<div style={{ pointerEvents: "all", display: "flex" }}>
<input
type="color"
value={editor.user.getUserPreferences().color}
onChange={(e) => {
editor.user.updateUserPreferences({
color: e.currentTarget.value,
})
}}
/>
<input
value={editor.user.getUserPreferences().name}
onChange={(e) => {
editor.user.updateUserPreferences({
name: e.currentTarget.value,
})
}}
/>
</div>
)
})

69
src/TLStoreToAutomerge.ts Normal file
View File

@ -0,0 +1,69 @@
import { RecordsDiff, TLRecord } from "@tldraw/tldraw"
import _ from "lodash"
export function applyTLStoreChangesToAutomerge(
doc: any,
changes: RecordsDiff<TLRecord>
) {
Object.values(changes.added).forEach((record) => {
doc.store[record.id] = record
})
Object.values(changes.updated).forEach(([_, record]) => {
deepCompareAndUpdate(doc.store[record.id], record)
})
Object.values(changes.removed).forEach((record) => {
delete doc.store[record.id]
})
}
function deepCompareAndUpdate(objectA: any, objectB: any) {
// eslint-disable-line
if (_.isArray(objectB)) {
if (!_.isArray(objectA)) {
// if objectA is not an array, replace it with objectB
objectA = objectB.slice()
} else {
// compare and update array elements
for (let i = 0; i < objectB.length; i++) {
if (i >= objectA.length) {
objectA.push(objectB[i])
} else {
if (_.isObject(objectB[i]) || _.isArray(objectB[i])) {
// if element is an object or array, recursively compare and update
deepCompareAndUpdate(objectA[i], objectB[i])
} else if (objectA[i] !== objectB[i]) {
// update the element
objectA[i] = objectB[i]
}
}
}
// remove extra elements
if (objectA.length > objectB.length) {
objectA.splice(objectB.length)
}
}
} else if (_.isObject(objectB)) {
_.forIn(objectB, (value: any, key: any) => {
if (objectA[key] === undefined) {
// if key is not in objectA, add it
objectA[key] = value
} else {
if (_.isObject(value) || _.isArray(value)) {
// if value is an object or array, recursively compare and update
deepCompareAndUpdate(objectA[key], value)
} else if (objectA[key] !== value) {
// update the value
objectA[key] = value
}
}
})
_.forIn(objectA, (_: any, key: string) => {
if ((objectB as any)[key] === undefined) {
// if key is not in objectB, remove it
delete objectA[key]
}
})
}
}

141
src/default_store.ts Normal file
View File

@ -0,0 +1,141 @@
export const DEFAULT_STORE = {
store: {
"document:document": {
gridSize: 10,
name: "",
meta: {},
id: "document:document",
typeName: "document",
},
"pointer:pointer": {
id: "pointer:pointer",
typeName: "pointer",
x: 0,
y: 0,
lastActivityTimestamp: 0,
meta: {},
},
"page:page": {
meta: {},
id: "page:page",
name: "Page 1",
index: "a1",
typeName: "page",
},
"camera:page:page": {
x: 0,
y: 0,
z: 1,
meta: {},
id: "camera:page:page",
typeName: "camera",
},
"instance_page_state:page:page": {
editingShapeId: null,
croppingShapeId: null,
selectedShapeIds: [],
hoveredShapeId: null,
erasingShapeIds: [],
hintingShapeIds: [],
focusedGroupId: null,
meta: {},
id: "instance_page_state:page:page",
pageId: "page:page",
typeName: "instance_page_state",
},
"instance:instance": {
followingUserId: null,
opacityForNextShape: 1,
stylesForNextShape: {},
brush: null,
scribble: null,
cursor: {
type: "default",
rotation: 0,
},
isFocusMode: false,
exportBackground: true,
isDebugMode: false,
isToolLocked: false,
screenBounds: {
x: 0,
y: 0,
w: 720,
h: 400,
},
zoomBrush: null,
isGridMode: false,
isPenMode: false,
chatMessage: "",
isChatting: false,
highlightedUserIds: [],
canMoveCamera: true,
isFocused: true,
devicePixelRatio: 2,
isCoarsePointer: false,
isHoveringCanvas: false,
openMenus: [],
isChangingStyle: false,
isReadonly: false,
meta: {},
id: "instance:instance",
currentPageId: "page:page",
typeName: "instance",
},
},
schema: {
schemaVersion: 1,
storeVersion: 4,
recordVersions: {
asset: {
version: 1,
subTypeKey: "type",
subTypeVersions: {
image: 2,
video: 2,
bookmark: 0,
},
},
camera: {
version: 1,
},
document: {
version: 2,
},
instance: {
version: 21,
},
instance_page_state: {
version: 5,
},
page: {
version: 1,
},
shape: {
version: 3,
subTypeKey: "type",
subTypeVersions: {
group: 0,
text: 1,
bookmark: 1,
draw: 1,
geo: 7,
note: 4,
line: 1,
frame: 0,
arrow: 1,
highlight: 0,
embed: 4,
image: 2,
video: 1,
},
},
instance_presence: {
version: 5,
},
pointer: {
version: 1,
},
},
},
}

183
src/useAutomergeStore.ts Normal file
View File

@ -0,0 +1,183 @@
import {
TLAnyShapeUtilConstructor,
TLRecord,
TLStoreWithStatus,
createTLStore,
defaultShapeUtils,
HistoryEntry,
getUserPreferences,
setUserPreferences,
defaultUserPreferences,
createPresenceStateDerivation,
InstancePresenceRecordType,
computed,
react,
TLStoreSnapshot,
sortById,
} from "@tldraw/tldraw"
import { useEffect, useState } from "react"
import { DocHandle, DocHandleChangePayload } from "@automerge/automerge-repo"
import {
useLocalAwareness,
useRemoteAwareness,
} from "@automerge/automerge-repo-react-hooks"
import { applyAutomergePatchesToTLStore } from "./AutomergeToTLStore.js"
import { applyTLStoreChangesToAutomerge } from "./TLStoreToAutomerge.js"
export function useAutomergeStore({
handle,
shapeUtils = [],
}: {
handle: DocHandle<TLStoreSnapshot>
userId: string
shapeUtils?: TLAnyShapeUtilConstructor[]
}): TLStoreWithStatus {
const [store] = useState(() => {
const store = createTLStore({
shapeUtils: [...defaultShapeUtils, ...shapeUtils],
})
return store
})
const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({
status: "loading",
})
/* -------------------- TLDraw <--> Automerge -------------------- */
useEffect(() => {
const unsubs: (() => void)[] = []
// A hacky workaround to prevent local changes from being applied twice
// once into the automerge doc and then back again.
let preventPatchApplications = false
/* TLDraw to Automerge */
function syncStoreChangesToAutomergeDoc({
changes,
}: HistoryEntry<TLRecord>) {
preventPatchApplications = true
handle.change((doc) => {
applyTLStoreChangesToAutomerge(doc, changes)
})
preventPatchApplications = false
}
unsubs.push(
store.listen(syncStoreChangesToAutomergeDoc, {
source: "user",
scope: "document",
})
)
/* Automerge to TLDraw */
const syncAutomergeDocChangesToStore = ({
patches,
}: DocHandleChangePayload<any>) => {
if (preventPatchApplications) return
applyAutomergePatchesToTLStore(patches, store)
}
handle.on("change", syncAutomergeDocChangesToStore)
unsubs.push(() => handle.off("change", syncAutomergeDocChangesToStore))
/* Defer rendering until the document is ready */
// TODO: need to think through the various status possibilities here and how they map
handle.whenReady().then(() => {
const doc = handle.docSync()
if (!doc) throw new Error("Document not found")
if (!doc.store) throw new Error("Document store not initialized")
store.mergeRemoteChanges(() => {
store.loadSnapshot({
store: JSON.parse(JSON.stringify(doc.store)),
schema: doc.schema,
})
})
setStoreWithStatus({
store,
status: "synced-remote",
connectionStatus: "online",
})
})
return () => {
unsubs.forEach((fn) => fn())
unsubs.length = 0
}
}, [handle, store])
return storeWithStatus
}
export function useAutomergePresence({ handle, store, userMetadata }:
{ handle: DocHandle<TLStoreSnapshot>, store: TLStoreWithStatus, userMetadata: any }) {
const innerStore = store?.store
const { userId, name, color } = userMetadata
const [, updateLocalState] = useLocalAwareness({
handle,
userId,
initialState: {},
})
const [peerStates] = useRemoteAwareness({
handle,
localUserId: userId,
})
/* ----------- Presence stuff ----------- */
useEffect(() => {
if (!innerStore) return
const toPut: TLRecord[] =
Object.values(peerStates)
.filter((record) => record && Object.keys(record).length !== 0)
// put / remove the records in the store
const toRemove = innerStore.query.records('instance_presence').get().sort(sortById)
.map((record) => record.id)
.filter((id) => !toPut.find((record) => record.id === id))
if (toRemove.length) innerStore.remove(toRemove)
if (toPut.length) innerStore.put(toPut)
}, [innerStore, peerStates])
useEffect(() => {
if (!innerStore) return
/* ----------- Presence stuff ----------- */
setUserPreferences({ id: userId, color, name })
const userPreferences = computed<{
id: string
color: string
name: string
}>("userPreferences", () => {
const user = getUserPreferences()
return {
id: user.id,
color: user.color ?? defaultUserPreferences.color,
name: user.name ?? defaultUserPreferences.name,
}
})
const presenceId = InstancePresenceRecordType.createId(userId)
const presenceDerivation = createPresenceStateDerivation(
userPreferences,
presenceId
)(innerStore)
return react("when presence changes", () => {
const presence = presenceDerivation.get()
requestAnimationFrame(() => {
updateLocalState(presence)
})
})
}, [innerStore, userId, updateLocalState])
/* ----------- End presence stuff ----------- */
}