shared piano in progress
This commit is contained in:
parent
af2a93aa1a
commit
2db320a007
|
|
@ -10,6 +10,7 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.33.1",
|
"@anthropic-ai/sdk": "^0.33.1",
|
||||||
|
"@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",
|
||||||
|
|
@ -182,6 +183,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 +524,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 +1967,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",
|
||||||
|
|
@ -4400,6 +4541,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 +4655,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 +4774,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 +6727,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",
|
||||||
|
|
@ -9155,6 +9358,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 +10084,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",
|
||||||
|
|
@ -11526,7 +11753,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 +12683,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",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.33.1",
|
"@anthropic-ai/sdk": "^0.33.1",
|
||||||
|
"@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 +29,8 @@ import { SlideShape } from "@/shapes/SlideShapeUtil"
|
||||||
import { makeRealSettings, applySettingsMigrations } from "@/lib/settings"
|
import { makeRealSettings, applySettingsMigrations } from "@/lib/settings"
|
||||||
import { PromptShapeTool } from "@/tools/PromptShapeTool"
|
import { PromptShapeTool } from "@/tools/PromptShapeTool"
|
||||||
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
||||||
|
import { SharedPianoTool } from "@/tools/SharedPianoTool"
|
||||||
|
import { SharedPianoShape } from "@/shapes/SharedPianoShapeUtil"
|
||||||
import {
|
import {
|
||||||
lockElement,
|
lockElement,
|
||||||
unlockElement,
|
unlockElement,
|
||||||
|
|
@ -58,6 +60,7 @@ const customShapeUtils = [
|
||||||
MycrozineTemplateShape,
|
MycrozineTemplateShape,
|
||||||
MarkdownShape,
|
MarkdownShape,
|
||||||
PromptShape,
|
PromptShape,
|
||||||
|
SharedPianoShape,
|
||||||
]
|
]
|
||||||
const customTools = [
|
const customTools = [
|
||||||
ChatBoxTool,
|
ChatBoxTool,
|
||||||
|
|
@ -67,6 +70,7 @@ const customTools = [
|
||||||
MycrozineTemplateTool,
|
MycrozineTemplateTool,
|
||||||
MarkdownTool,
|
MarkdownTool,
|
||||||
PromptShapeTool,
|
PromptShapeTool,
|
||||||
|
SharedPianoTool,
|
||||||
GestureTool,
|
GestureTool,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,257 @@
|
||||||
|
import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"
|
||||||
|
import { useCallback, useState } from "react"
|
||||||
|
|
||||||
|
export type ISharedPianoShape = TLBaseShape<
|
||||||
|
"SharedPiano",
|
||||||
|
{
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
isMinimized?: boolean
|
||||||
|
interactionState?: {
|
||||||
|
scrollPosition?: { x: number; y: number }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
const getDefaultDimensions = (): { w: number; h: number } => {
|
||||||
|
// Default dimensions for the Shared Piano (16:9 ratio)
|
||||||
|
return { w: 800, h: 600 }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SharedPianoShape extends BaseBoxShapeUtil<ISharedPianoShape> {
|
||||||
|
static override type = "SharedPiano"
|
||||||
|
|
||||||
|
getDefaultProps(): ISharedPianoShape["props"] {
|
||||||
|
const { w, h } = getDefaultDimensions()
|
||||||
|
return {
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
isMinimized: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator(shape: ISharedPianoShape) {
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
width={shape.props.w}
|
||||||
|
height={shape.props.h}
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--color-selected)"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
component(shape: ISharedPianoShape) {
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const handleIframeLoad = useCallback(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
setError(null)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleIframeError = useCallback(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
setError("Failed to load Shared Piano")
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleToggleMinimize = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
this.editor.updateShape<ISharedPianoShape>({
|
||||||
|
id: shape.id,
|
||||||
|
type: "SharedPiano",
|
||||||
|
props: {
|
||||||
|
...shape.props,
|
||||||
|
isMinimized: !shape.props.isMinimized,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const controls = (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
zIndex: 1000,
|
||||||
|
display: "flex",
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={handleToggleMinimize}
|
||||||
|
style={{
|
||||||
|
background: "rgba(0, 0, 0, 0.7)",
|
||||||
|
color: "white",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "4px",
|
||||||
|
padding: "4px 8px",
|
||||||
|
fontSize: "12px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{shape.props.isMinimized ? "🔽" : "🔼"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const sharedPianoUrl = "https://musiclab.chromeexperiments.com/Shared-Piano/#jQB715bFJ"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
position: "relative",
|
||||||
|
overflow: "hidden",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "1px solid var(--color-panel)",
|
||||||
|
backgroundColor: "var(--color-background)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{controls}
|
||||||
|
|
||||||
|
{shape.props.isMinimized ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||||
|
color: "white",
|
||||||
|
fontSize: "16px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🎹 Shared Piano
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{isLoading && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background: "var(--color-background)",
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: "center" }}>
|
||||||
|
<div style={{ fontSize: "24px", marginBottom: "8px" }}>🎹</div>
|
||||||
|
<div>Loading Shared Piano...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background: "var(--color-background)",
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: "center", color: "var(--color-text)" }}>
|
||||||
|
<div style={{ fontSize: "24px", marginBottom: "8px" }}>❌</div>
|
||||||
|
<div>{error}</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsLoading(true)
|
||||||
|
setError(null)
|
||||||
|
// Force iframe reload
|
||||||
|
const iframe = document.querySelector(`iframe[data-shape-id="${shape.id}"]`) as HTMLIFrameElement
|
||||||
|
if (iframe) {
|
||||||
|
iframe.src = iframe.src
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
marginTop: "8px",
|
||||||
|
padding: "4px 8px",
|
||||||
|
background: "var(--color-primary)",
|
||||||
|
color: "white",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "4px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<iframe
|
||||||
|
data-shape-id={shape.id}
|
||||||
|
src={sharedPianoUrl}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "8px",
|
||||||
|
opacity: isLoading ? 0 : 1,
|
||||||
|
transition: "opacity 0.3s ease",
|
||||||
|
}}
|
||||||
|
onLoad={handleIframeLoad}
|
||||||
|
onError={handleIframeError}
|
||||||
|
title="Chrome Music Lab Shared Piano"
|
||||||
|
allow="microphone; camera"
|
||||||
|
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override onDoubleClick = (shape: ISharedPianoShape) => {
|
||||||
|
// Toggle minimized state on double click
|
||||||
|
this.editor.updateShape<ISharedPianoShape>({
|
||||||
|
id: shape.id,
|
||||||
|
type: "SharedPiano",
|
||||||
|
props: {
|
||||||
|
...shape.props,
|
||||||
|
isMinimized: !shape.props.isMinimized,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onPointerDown = (_shape: ISharedPianoShape) => {
|
||||||
|
// Handle pointer down events if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
override onBeforeCreate = (shape: ISharedPianoShape) => {
|
||||||
|
// Set default dimensions if not provided
|
||||||
|
if (!shape.props.w || !shape.props.h) {
|
||||||
|
const { w, h } = getDefaultDimensions()
|
||||||
|
this.editor.updateShape<ISharedPianoShape>({
|
||||||
|
id: shape.id,
|
||||||
|
type: "SharedPiano",
|
||||||
|
props: {
|
||||||
|
...shape.props,
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUpdate = (_prev: ISharedPianoShape, _next: ISharedPianoShape) => {
|
||||||
|
// Handle any updates if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,15 @@ export type IVideoChatShape = TLBaseShape<
|
||||||
allowMicrophone: boolean
|
allowMicrophone: boolean
|
||||||
enableRecording: boolean
|
enableRecording: boolean
|
||||||
recordingId: string | null // Track active recording
|
recordingId: string | null // Track active recording
|
||||||
|
enableTranscription: boolean
|
||||||
|
isTranscribing: boolean
|
||||||
|
transcriptionHistory: Array<{
|
||||||
|
sender: string
|
||||||
|
message: string
|
||||||
|
id: string
|
||||||
|
}>
|
||||||
|
meetingToken: string | null
|
||||||
|
isOwner: boolean
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
@ -30,15 +39,40 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultProps(): IVideoChatShape["props"] {
|
getDefaultProps(): IVideoChatShape["props"] {
|
||||||
return {
|
const props = {
|
||||||
roomUrl: null,
|
roomUrl: null,
|
||||||
w: 800,
|
w: 800,
|
||||||
h: 600,
|
h: 600,
|
||||||
allowCamera: false,
|
allowCamera: false,
|
||||||
allowMicrophone: false,
|
allowMicrophone: false,
|
||||||
enableRecording: true,
|
enableRecording: true,
|
||||||
recordingId: null
|
recordingId: null,
|
||||||
|
enableTranscription: true,
|
||||||
|
isTranscribing: false,
|
||||||
|
transcriptionHistory: [],
|
||||||
|
meetingToken: null,
|
||||||
|
isOwner: false
|
||||||
|
};
|
||||||
|
console.log('🔧 getDefaultProps called, returning:', props);
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateMeetingToken(roomName: string, isOwner: boolean = false) {
|
||||||
|
const workerUrl = import.meta.env.VITE_TLDRAW_WORKER_URL;
|
||||||
|
const apiKey = import.meta.env.VITE_DAILY_API_KEY;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error('Daily.co API key not configured');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!workerUrl) {
|
||||||
|
throw new Error('Worker URL is not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, let's skip token generation and use a simpler approach
|
||||||
|
// We'll use the room URL directly and handle owner permissions differently
|
||||||
|
console.log('Skipping meeting token generation for now');
|
||||||
|
return `token_${roomName}_${Date.now()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureRoomExists(shape: IVideoChatShape) {
|
async ensureRoomExists(shape: IVideoChatShape) {
|
||||||
|
|
@ -50,8 +84,9 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
// Try to get existing room URL from localStorage first
|
// Try to get existing room URL from localStorage first
|
||||||
const storageKey = `videoChat_room_${boardId}`;
|
const storageKey = `videoChat_room_${boardId}`;
|
||||||
const existingRoomUrl = localStorage.getItem(storageKey);
|
const existingRoomUrl = localStorage.getItem(storageKey);
|
||||||
|
const existingToken = localStorage.getItem(`${storageKey}_token`);
|
||||||
|
|
||||||
if (existingRoomUrl && existingRoomUrl !== 'undefined') {
|
if (existingRoomUrl && existingRoomUrl !== 'undefined' && existingToken) {
|
||||||
console.log("Using existing room from storage:", existingRoomUrl);
|
console.log("Using existing room from storage:", existingRoomUrl);
|
||||||
await this.editor.updateShape<IVideoChatShape>({
|
await this.editor.updateShape<IVideoChatShape>({
|
||||||
id: shape.id,
|
id: shape.id,
|
||||||
|
|
@ -59,14 +94,17 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
props: {
|
props: {
|
||||||
...shape.props,
|
...shape.props,
|
||||||
roomUrl: existingRoomUrl,
|
roomUrl: existingRoomUrl,
|
||||||
|
meetingToken: existingToken,
|
||||||
|
isOwner: true, // Assume the creator is the owner
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shape.props.roomUrl !== null && shape.props.roomUrl !== 'undefined') {
|
if (shape.props.roomUrl !== null && shape.props.roomUrl !== 'undefined' && shape.props.meetingToken) {
|
||||||
console.log("Room already exists:", shape.props.roomUrl);
|
console.log("Room already exists:", shape.props.roomUrl);
|
||||||
localStorage.setItem(storageKey, shape.props.roomUrl);
|
localStorage.setItem(storageKey, shape.props.roomUrl);
|
||||||
|
localStorage.setItem(`${storageKey}_token`, shape.props.meetingToken);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,7 +145,11 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
format: "mp4",
|
format: "mp4",
|
||||||
mode: "audio-only"
|
mode: "audio-only"
|
||||||
},
|
},
|
||||||
auto_start_transcription: true,
|
// Transcription settings
|
||||||
|
transcription: {
|
||||||
|
enabled: true,
|
||||||
|
auto_start: false
|
||||||
|
},
|
||||||
recordings_template: "{room_name}/audio-{epoch_time}.mp4"
|
recordings_template: "{room_name}/audio-{epoch_time}.mp4"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -123,11 +165,18 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
|
|
||||||
if (!url) throw new Error("Room URL is missing")
|
if (!url) throw new Error("Room URL is missing")
|
||||||
|
|
||||||
// Store the room URL in localStorage
|
// Generate meeting token for the owner
|
||||||
|
// First ensure the room exists, then generate token
|
||||||
|
const meetingToken = await this.generateMeetingToken(roomName, true);
|
||||||
|
|
||||||
|
// Store the room URL and token in localStorage
|
||||||
localStorage.setItem(storageKey, url);
|
localStorage.setItem(storageKey, url);
|
||||||
|
localStorage.setItem(`${storageKey}_token`, meetingToken);
|
||||||
|
|
||||||
console.log("Room created successfully:", url)
|
console.log("Room created successfully:", url)
|
||||||
console.log("Updating shape with new URL")
|
console.log("Meeting token generated:", meetingToken)
|
||||||
|
console.log("Updating shape with new URL and token")
|
||||||
|
console.log("Setting isOwner to true")
|
||||||
|
|
||||||
await this.editor.updateShape<IVideoChatShape>({
|
await this.editor.updateShape<IVideoChatShape>({
|
||||||
id: shape.id,
|
id: shape.id,
|
||||||
|
|
@ -135,10 +184,14 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
props: {
|
props: {
|
||||||
...shape.props,
|
...shape.props,
|
||||||
roomUrl: url,
|
roomUrl: url,
|
||||||
|
meetingToken: meetingToken,
|
||||||
|
isOwner: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log("Shape updated:", this.editor.getShape(shape.id))
|
console.log("Shape updated:", this.editor.getShape(shape.id))
|
||||||
|
const updatedShape = this.editor.getShape(shape.id) as IVideoChatShape;
|
||||||
|
console.log("Updated shape isOwner:", updatedShape?.props.isOwner)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in ensureRoomExists:", error)
|
console.error("Error in ensureRoomExists:", error)
|
||||||
throw error
|
throw error
|
||||||
|
|
@ -214,6 +267,151 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async startTranscription(shape: IVideoChatShape) {
|
||||||
|
console.log('🎤 startTranscription method called');
|
||||||
|
console.log('Shape props:', shape.props);
|
||||||
|
console.log('Room URL:', shape.props.roomUrl);
|
||||||
|
console.log('Is owner:', shape.props.isOwner);
|
||||||
|
|
||||||
|
if (!shape.props.roomUrl || !shape.props.isOwner) {
|
||||||
|
console.log('❌ Early return - missing roomUrl or not owner');
|
||||||
|
console.log('roomUrl exists:', !!shape.props.roomUrl);
|
||||||
|
console.log('isOwner:', shape.props.isOwner);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const workerUrl = import.meta.env.VITE_TLDRAW_WORKER_URL;
|
||||||
|
const apiKey = import.meta.env.VITE_DAILY_API_KEY;
|
||||||
|
|
||||||
|
console.log('🔧 Environment variables:');
|
||||||
|
console.log('Worker URL:', workerUrl);
|
||||||
|
console.log('API Key exists:', !!apiKey);
|
||||||
|
|
||||||
|
// Extract room name from URL
|
||||||
|
const roomName = shape.props.roomUrl.split('/').pop();
|
||||||
|
console.log('📝 Extracted room name:', roomName);
|
||||||
|
|
||||||
|
if (!roomName) {
|
||||||
|
throw new Error('Could not extract room name from URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🌐 Making API request to start transcription...');
|
||||||
|
console.log('Request URL:', `${workerUrl}/daily/rooms/${roomName}/start-transcription`);
|
||||||
|
|
||||||
|
const response = await fetch(`${workerUrl}/daily/rooms/${roomName}/start-transcription`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📡 Response status:', response.status);
|
||||||
|
console.log('📡 Response ok:', response.ok);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
console.error('❌ API error response:', error);
|
||||||
|
throw new Error(`Failed to start transcription: ${JSON.stringify(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ API call successful, updating shape...');
|
||||||
|
await this.editor.updateShape<IVideoChatShape>({
|
||||||
|
id: shape.id,
|
||||||
|
type: shape.type,
|
||||||
|
props: {
|
||||||
|
...shape.props,
|
||||||
|
isTranscribing: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('✅ Shape updated with isTranscribing: true');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error starting transcription:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopTranscription(shape: IVideoChatShape) {
|
||||||
|
console.log('🛑 stopTranscription method called');
|
||||||
|
console.log('Shape props:', shape.props);
|
||||||
|
|
||||||
|
if (!shape.props.roomUrl || !shape.props.isOwner) {
|
||||||
|
console.log('❌ Early return - missing roomUrl or not owner');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const workerUrl = import.meta.env.VITE_TLDRAW_WORKER_URL;
|
||||||
|
const apiKey = import.meta.env.VITE_DAILY_API_KEY;
|
||||||
|
|
||||||
|
// Extract room name from URL
|
||||||
|
const roomName = shape.props.roomUrl.split('/').pop();
|
||||||
|
console.log('📝 Extracted room name:', roomName);
|
||||||
|
|
||||||
|
if (!roomName) {
|
||||||
|
throw new Error('Could not extract room name from URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🌐 Making API request to stop transcription...');
|
||||||
|
const response = await fetch(`${workerUrl}/daily/rooms/${roomName}/stop-transcription`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📡 Response status:', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
console.error('❌ API error response:', error);
|
||||||
|
throw new Error(`Failed to stop transcription: ${JSON.stringify(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ API call successful, updating shape...');
|
||||||
|
await this.editor.updateShape<IVideoChatShape>({
|
||||||
|
id: shape.id,
|
||||||
|
type: shape.type,
|
||||||
|
props: {
|
||||||
|
...shape.props,
|
||||||
|
isTranscribing: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('✅ Shape updated with isTranscribing: false');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error stopping transcription:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addTranscriptionMessage(shape: IVideoChatShape, sender: string, message: string) {
|
||||||
|
console.log('📝 addTranscriptionMessage called');
|
||||||
|
console.log('Sender:', sender);
|
||||||
|
console.log('Message:', message);
|
||||||
|
console.log('Current transcription history length:', shape.props.transcriptionHistory.length);
|
||||||
|
|
||||||
|
const newMessage = {
|
||||||
|
sender,
|
||||||
|
message,
|
||||||
|
id: `${Date.now()}_${Math.random()}`
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('📝 Adding new message:', newMessage);
|
||||||
|
|
||||||
|
this.editor.updateShape<IVideoChatShape>({
|
||||||
|
id: shape.id,
|
||||||
|
type: shape.type,
|
||||||
|
props: {
|
||||||
|
...shape.props,
|
||||||
|
transcriptionHistory: [...shape.props.transcriptionHistory, newMessage]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Transcription message added to shape');
|
||||||
|
}
|
||||||
|
|
||||||
component(shape: IVideoChatShape) {
|
component(shape: IVideoChatShape) {
|
||||||
const [hasPermissions, setHasPermissions] = useState(false)
|
const [hasPermissions, setHasPermissions] = useState(false)
|
||||||
const [error, setError] = useState<Error | null>(null)
|
const [error, setError] = useState<Error | null>(null)
|
||||||
|
|
@ -344,6 +542,7 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
}`}
|
}`}
|
||||||
></iframe>
|
></iframe>
|
||||||
|
|
||||||
|
{/* Recording Button */}
|
||||||
{shape.props.enableRecording && (
|
{shape.props.enableRecording && (
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
|
@ -373,6 +572,105 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Test Button - Always visible for debugging */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
console.log('🧪 Test button clicked!');
|
||||||
|
console.log('Shape props:', shape.props);
|
||||||
|
alert('Test button clicked! Check console for details.');
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "8px",
|
||||||
|
left: "8px",
|
||||||
|
padding: "4px 8px",
|
||||||
|
background: "#ffff00",
|
||||||
|
border: "1px solid #000",
|
||||||
|
borderRadius: "4px",
|
||||||
|
cursor: "pointer",
|
||||||
|
zIndex: 1000,
|
||||||
|
fontSize: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
TEST
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Transcription Button - Only for owners */}
|
||||||
|
{(() => {
|
||||||
|
console.log('🔍 Checking transcription button conditions:');
|
||||||
|
console.log('enableTranscription:', shape.props.enableTranscription);
|
||||||
|
console.log('isOwner:', shape.props.isOwner);
|
||||||
|
console.log('Button should render:', shape.props.enableTranscription && shape.props.isOwner);
|
||||||
|
return shape.props.enableTranscription && shape.props.isOwner;
|
||||||
|
})() && (
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
console.log('🚀 Transcription button clicked!');
|
||||||
|
console.log('Current transcription state:', shape.props.isTranscribing);
|
||||||
|
console.log('Shape props:', shape.props);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (shape.props.isTranscribing) {
|
||||||
|
console.log('🛑 Stopping transcription...');
|
||||||
|
await this.stopTranscription(shape);
|
||||||
|
console.log('✅ Transcription stopped successfully');
|
||||||
|
} else {
|
||||||
|
console.log('🎤 Starting transcription...');
|
||||||
|
await this.startTranscription(shape);
|
||||||
|
console.log('✅ Transcription started successfully');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Transcription error:', err);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "8px",
|
||||||
|
right: shape.props.enableRecording ? "120px" : "8px",
|
||||||
|
padding: "4px 8px",
|
||||||
|
background: shape.props.isTranscribing ? "#44ff44" : "#ffffff",
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
borderRadius: "4px",
|
||||||
|
cursor: "pointer",
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{shape.props.isTranscribing ? "Stop Transcription" : "Start Transcription"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Transcription History */}
|
||||||
|
{shape.props.transcriptionHistory.length > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: "40px",
|
||||||
|
left: "8px",
|
||||||
|
right: "8px",
|
||||||
|
maxHeight: "200px",
|
||||||
|
overflowY: "auto",
|
||||||
|
background: "rgba(255, 255, 255, 0.95)",
|
||||||
|
borderRadius: "4px",
|
||||||
|
padding: "8px",
|
||||||
|
fontSize: "12px",
|
||||||
|
zIndex: 1,
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>
|
||||||
|
Live Transcription:
|
||||||
|
</div>
|
||||||
|
{shape.props.transcriptionHistory.slice(-10).map((msg) => (
|
||||||
|
<div key={msg.id} style={{ marginBottom: "2px" }}>
|
||||||
|
<span style={{ fontWeight: "bold", color: "#666" }}>
|
||||||
|
{msg.sender}:
|
||||||
|
</span>{" "}
|
||||||
|
<span>{msg.message}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<p
|
<p
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
|
@ -390,6 +688,7 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
url: {roomUrl}
|
url: {roomUrl}
|
||||||
|
{shape.props.isOwner && " (Owner)"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { BaseBoxShapeTool } from "tldraw"
|
||||||
|
|
||||||
|
export class SharedPianoTool extends BaseBoxShapeTool {
|
||||||
|
static override id = "SharedPiano"
|
||||||
|
shapeType = "SharedPiano"
|
||||||
|
override initial = "idle"
|
||||||
|
}
|
||||||
|
|
@ -168,6 +168,14 @@ export function CustomToolbar() {
|
||||||
isSelected={tools["Prompt"].id === editor.getCurrentToolId()}
|
isSelected={tools["Prompt"].id === editor.getCurrentToolId()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{tools["SharedPiano"] && (
|
||||||
|
<TldrawUiMenuItem
|
||||||
|
{...tools["SharedPiano"]}
|
||||||
|
icon="music"
|
||||||
|
label="Shared Piano"
|
||||||
|
isSelected={tools["SharedPiano"].id === editor.getCurrentToolId()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</DefaultToolbar>
|
</DefaultToolbar>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,15 @@ export const overrides: TLUiOverrides = {
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect: () => editor.setCurrentTool("Prompt"),
|
onSelect: () => editor.setCurrentTool("Prompt"),
|
||||||
},
|
},
|
||||||
|
SharedPiano: {
|
||||||
|
id: "SharedPiano",
|
||||||
|
icon: "music",
|
||||||
|
label: "Shared Piano",
|
||||||
|
type: "SharedPiano",
|
||||||
|
kbd: "alt+p",
|
||||||
|
readonlyOk: true,
|
||||||
|
onSelect: () => editor.setCurrentTool("SharedPiano"),
|
||||||
|
},
|
||||||
gesture: {
|
gesture: {
|
||||||
id: "gesture",
|
id: "gesture",
|
||||||
icon: "draw",
|
icon: "draw",
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { MarkdownShape } from "@/shapes/MarkdownShapeUtil"
|
||||||
import { MycrozineTemplateShape } from "@/shapes/MycrozineTemplateShapeUtil"
|
import { MycrozineTemplateShape } from "@/shapes/MycrozineTemplateShapeUtil"
|
||||||
import { SlideShape } from "@/shapes/SlideShapeUtil"
|
import { SlideShape } from "@/shapes/SlideShapeUtil"
|
||||||
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
||||||
|
import { SharedPianoShape } from "@/shapes/SharedPianoShapeUtil"
|
||||||
|
|
||||||
// add custom shapes and bindings here if needed:
|
// add custom shapes and bindings here if needed:
|
||||||
export const customSchema = createTLSchema({
|
export const customSchema = createTLSchema({
|
||||||
|
|
@ -52,6 +53,10 @@ export const customSchema = createTLSchema({
|
||||||
props: PromptShape.props,
|
props: PromptShape.props,
|
||||||
migrations: PromptShape.migrations,
|
migrations: PromptShape.migrations,
|
||||||
},
|
},
|
||||||
|
SharedPiano: {
|
||||||
|
props: SharedPianoShape.props,
|
||||||
|
migrations: SharedPianoShape.migrations,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
bindings: defaultBindingSchemas,
|
bindings: defaultBindingSchemas,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
126
worker/worker.ts
126
worker/worker.ts
|
|
@ -162,6 +162,50 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
.post("/daily/tokens", async (req) => {
|
||||||
|
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return new Response(JSON.stringify({ error: 'No API key provided' }), {
|
||||||
|
status: 401,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await req.json()
|
||||||
|
const response = await fetch('https://api.daily.co/v1/meeting-tokens', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
room_name: body.room_name,
|
||||||
|
properties: body.properties
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
return new Response(JSON.stringify(error), {
|
||||||
|
status: response.status,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Add new transcription endpoints
|
// Add new transcription endpoints
|
||||||
.post("/daily/rooms/:roomName/start-transcription", async (req) => {
|
.post("/daily/rooms/:roomName/start-transcription", async (req) => {
|
||||||
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
||||||
|
|
@ -325,6 +369,88 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Recording endpoints
|
||||||
|
.post("/daily/recordings/start", async (req) => {
|
||||||
|
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return new Response(JSON.stringify({ error: 'No API key provided' }), {
|
||||||
|
status: 401,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await req.json()
|
||||||
|
const response = await fetch('https://api.daily.co/v1/recordings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
return new Response(JSON.stringify(error), {
|
||||||
|
status: response.status,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
.post("/daily/recordings/:recordingId/stop", async (req) => {
|
||||||
|
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
||||||
|
const { recordingId } = req.params
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return new Response(JSON.stringify({ error: 'No API key provided' }), {
|
||||||
|
status: 401,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://api.daily.co/v1/recordings/${recordingId}/stop`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
return new Response(JSON.stringify(error), {
|
||||||
|
status: response.status,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function backupAllBoards(env: Environment) {
|
async function backupAllBoards(env: Environment) {
|
||||||
try {
|
try {
|
||||||
// List all room files from TLDRAW_BUCKET
|
// List all room files from TLDRAW_BUCKET
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue