shared piano in progress
This commit is contained in:
parent
af2a93aa1a
commit
2db320a007
|
|
@ -10,6 +10,7 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.33.1",
|
||||
"@automerge/automerge-repo-react-hooks": "^2.2.0",
|
||||
"@daily-co/daily-js": "^0.60.0",
|
||||
"@daily-co/daily-react": "^0.20.0",
|
||||
"@tldraw/assets": "^3.6.0",
|
||||
|
|
@ -182,6 +183,56 @@
|
|||
"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": {
|
||||
"version": "7.26.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
||||
|
|
@ -473,6 +524,84 @@
|
|||
"license": "MIT",
|
||||
"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": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@cloudflare/intl-types/-/intl-types-1.5.6.tgz",
|
||||
|
|
@ -1838,6 +1967,18 @@
|
|||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"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": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
|
@ -4400,6 +4541,12 @@
|
|||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"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_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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
|
||||
|
|
@ -4608,6 +4774,37 @@
|
|||
"license": "MIT",
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
||||
|
|
@ -6530,6 +6727,12 @@
|
|||
"license": "MIT",
|
||||
"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": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
|
||||
|
|
@ -9155,6 +9358,21 @@
|
|||
"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": {
|
||||
"version": "2.0.19",
|
||||
"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": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
|
|
@ -11526,7 +11753,6 @@
|
|||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
|
|
@ -12457,6 +12683,16 @@
|
|||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"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": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.33.1",
|
||||
"@automerge/automerge-repo-react-hooks": "^2.2.0",
|
||||
"@daily-co/daily-js": "^0.60.0",
|
||||
"@daily-co/daily-react": "^0.20.0",
|
||||
"@tldraw/assets": "^3.6.0",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import { SlideShape } from "@/shapes/SlideShapeUtil"
|
|||
import { makeRealSettings, applySettingsMigrations } from "@/lib/settings"
|
||||
import { PromptShapeTool } from "@/tools/PromptShapeTool"
|
||||
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
||||
import { SharedPianoTool } from "@/tools/SharedPianoTool"
|
||||
import { SharedPianoShape } from "@/shapes/SharedPianoShapeUtil"
|
||||
import {
|
||||
lockElement,
|
||||
unlockElement,
|
||||
|
|
@ -58,6 +60,7 @@ const customShapeUtils = [
|
|||
MycrozineTemplateShape,
|
||||
MarkdownShape,
|
||||
PromptShape,
|
||||
SharedPianoShape,
|
||||
]
|
||||
const customTools = [
|
||||
ChatBoxTool,
|
||||
|
|
@ -67,6 +70,7 @@ const customTools = [
|
|||
MycrozineTemplateTool,
|
||||
MarkdownTool,
|
||||
PromptShapeTool,
|
||||
SharedPianoTool,
|
||||
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
|
||||
enableRecording: boolean
|
||||
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"] {
|
||||
return {
|
||||
const props = {
|
||||
roomUrl: null,
|
||||
w: 800,
|
||||
h: 600,
|
||||
allowCamera: false,
|
||||
allowMicrophone: false,
|
||||
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) {
|
||||
|
|
@ -50,8 +84,9 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
|||
// Try to get existing room URL from localStorage first
|
||||
const storageKey = `videoChat_room_${boardId}`;
|
||||
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);
|
||||
await this.editor.updateShape<IVideoChatShape>({
|
||||
id: shape.id,
|
||||
|
|
@ -59,14 +94,17 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
|||
props: {
|
||||
...shape.props,
|
||||
roomUrl: existingRoomUrl,
|
||||
meetingToken: existingToken,
|
||||
isOwner: true, // Assume the creator is the owner
|
||||
},
|
||||
});
|
||||
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);
|
||||
localStorage.setItem(storageKey, shape.props.roomUrl);
|
||||
localStorage.setItem(`${storageKey}_token`, shape.props.meetingToken);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +145,11 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
|||
format: "mp4",
|
||||
mode: "audio-only"
|
||||
},
|
||||
auto_start_transcription: true,
|
||||
// Transcription settings
|
||||
transcription: {
|
||||
enabled: true,
|
||||
auto_start: false
|
||||
},
|
||||
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")
|
||||
|
||||
// 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}_token`, meetingToken);
|
||||
|
||||
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>({
|
||||
id: shape.id,
|
||||
|
|
@ -135,10 +184,14 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
|||
props: {
|
||||
...shape.props,
|
||||
roomUrl: url,
|
||||
meetingToken: meetingToken,
|
||||
isOwner: true,
|
||||
},
|
||||
})
|
||||
|
||||
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) {
|
||||
console.error("Error in ensureRoomExists:", 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) {
|
||||
const [hasPermissions, setHasPermissions] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
|
@ -344,6 +542,7 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
|||
}`}
|
||||
></iframe>
|
||||
|
||||
{/* Recording Button */}
|
||||
{shape.props.enableRecording && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
|
|
@ -373,6 +572,105 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
|||
</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
|
||||
style={{
|
||||
position: "absolute",
|
||||
|
|
@ -390,6 +688,7 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
|||
}}
|
||||
>
|
||||
url: {roomUrl}
|
||||
{shape.props.isOwner && " (Owner)"}
|
||||
</p>
|
||||
</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()}
|
||||
/>
|
||||
)}
|
||||
{tools["SharedPiano"] && (
|
||||
<TldrawUiMenuItem
|
||||
{...tools["SharedPiano"]}
|
||||
icon="music"
|
||||
label="Shared Piano"
|
||||
isSelected={tools["SharedPiano"].id === editor.getCurrentToolId()}
|
||||
/>
|
||||
)}
|
||||
</DefaultToolbar>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -151,6 +151,15 @@ export const overrides: TLUiOverrides = {
|
|||
readonlyOk: true,
|
||||
onSelect: () => editor.setCurrentTool("Prompt"),
|
||||
},
|
||||
SharedPiano: {
|
||||
id: "SharedPiano",
|
||||
icon: "music",
|
||||
label: "Shared Piano",
|
||||
type: "SharedPiano",
|
||||
kbd: "alt+p",
|
||||
readonlyOk: true,
|
||||
onSelect: () => editor.setCurrentTool("SharedPiano"),
|
||||
},
|
||||
gesture: {
|
||||
id: "gesture",
|
||||
icon: "draw",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { MarkdownShape } from "@/shapes/MarkdownShapeUtil"
|
|||
import { MycrozineTemplateShape } from "@/shapes/MycrozineTemplateShapeUtil"
|
||||
import { SlideShape } from "@/shapes/SlideShapeUtil"
|
||||
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
||||
import { SharedPianoShape } from "@/shapes/SharedPianoShapeUtil"
|
||||
|
||||
// add custom shapes and bindings here if needed:
|
||||
export const customSchema = createTLSchema({
|
||||
|
|
@ -52,6 +53,10 @@ export const customSchema = createTLSchema({
|
|||
props: PromptShape.props,
|
||||
migrations: PromptShape.migrations,
|
||||
},
|
||||
SharedPiano: {
|
||||
props: SharedPianoShape.props,
|
||||
migrations: SharedPianoShape.migrations,
|
||||
},
|
||||
},
|
||||
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
|
||||
.post("/daily/rooms/:roomName/start-transcription", async (req) => {
|
||||
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) {
|
||||
try {
|
||||
// List all room files from TLDRAW_BUCKET
|
||||
|
|
|
|||
Loading…
Reference in New Issue