Fixed asset upload CORS for broken links, updated markdown tool, changed keyboard shortcuts & menu ordering
This commit is contained in:
parent
b9addbe417
commit
12d26d0643
File diff suppressed because it is too large
Load Diff
|
|
@ -26,6 +26,7 @@
|
|||
"@tldraw/tlschema": "^3.6.0",
|
||||
"@types/markdown-it": "^14.1.1",
|
||||
"@types/marked": "^5.0.2",
|
||||
"@uiw/react-md-editor": "^4.0.5",
|
||||
"@vercel/analytics": "^1.2.2",
|
||||
"ai": "^4.1.0",
|
||||
"cherry-markdown": "^0.8.57",
|
||||
|
|
@ -41,6 +42,7 @@
|
|||
"rbush": "^4.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^7.0.2",
|
||||
"recoil": "^0.7.7",
|
||||
"tldraw": "^3.6.0",
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ export class MarkdownShape extends BaseBoxShapeUtil<IMarkdownShape> {
|
|||
|
||||
getDefaultProps(): IMarkdownShape['props'] {
|
||||
return {
|
||||
w: 300,
|
||||
h: 200,
|
||||
w: 500,
|
||||
h: 400,
|
||||
text: '',
|
||||
}
|
||||
}
|
||||
|
|
@ -110,20 +110,28 @@ export class MarkdownShape extends BaseBoxShapeUtil<IMarkdownShape> {
|
|||
},
|
||||
})
|
||||
}}
|
||||
preview='edit'
|
||||
hideToolbar={true}
|
||||
preview='live'
|
||||
visibleDragbar={false}
|
||||
style={{
|
||||
height: '100%',
|
||||
height: 'auto',
|
||||
minHeight: '100%',
|
||||
border: 'none',
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
previewOptions={{
|
||||
style: {
|
||||
padding: '8px',
|
||||
backgroundColor: 'transparent',
|
||||
}
|
||||
}}
|
||||
textareaProps={{
|
||||
placeholder: "Enter markdown text...",
|
||||
style: {
|
||||
backgroundColor: 'transparent',
|
||||
height: '100%',
|
||||
padding: '12px',
|
||||
padding: '8px',
|
||||
lineHeight: '1.5',
|
||||
fontSize: '14px',
|
||||
height: 'auto',
|
||||
minHeight: '100%',
|
||||
resize: 'none',
|
||||
backgroundColor: 'transparent',
|
||||
}
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
|
|
|
|||
|
|
@ -68,6 +68,29 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
|
|||
return (
|
||||
<DefaultContextMenu {...props}>
|
||||
<DefaultContextMenuContent />
|
||||
|
||||
{/* Frames List - Moved to top */}
|
||||
<TldrawUiMenuGroup id="frames-list">
|
||||
<TldrawUiMenuSubmenu id="frames-dropdown" label="Shortcut to Frames">
|
||||
{getAllFrames(editor).map((frame) => (
|
||||
<TldrawUiMenuItem
|
||||
key={frame.id}
|
||||
id={`frame-${frame.id}`}
|
||||
label={frame.title}
|
||||
onSelect={() => {
|
||||
const shape = editor.getShape(frame.id)
|
||||
if (shape) {
|
||||
editor.zoomToBounds(editor.getShapePageBounds(shape)!, {
|
||||
animation: { duration: 400, easing: (t) => t * (2 - t) },
|
||||
})
|
||||
editor.select(frame.id)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</TldrawUiMenuSubmenu>
|
||||
</TldrawUiMenuGroup>
|
||||
|
||||
{/* Camera Controls Group */}
|
||||
<TldrawUiMenuGroup id="camera-controls">
|
||||
<TldrawUiMenuItem {...customActions.zoomToSelection} disabled={!hasSelection} />
|
||||
|
|
@ -90,27 +113,7 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
|
|||
<TldrawUiMenuItem {...tools.Prompt} disabled={hasSelection} />
|
||||
</TldrawUiMenuGroup>
|
||||
|
||||
{/* Frame Controls */}
|
||||
<TldrawUiMenuGroup id="frames-list">
|
||||
<TldrawUiMenuSubmenu id="frames-dropdown" label="Shortcut to Frames">
|
||||
{getAllFrames(editor).map((frame) => (
|
||||
<TldrawUiMenuItem
|
||||
key={frame.id}
|
||||
id={`frame-${frame.id}`}
|
||||
label={frame.title}
|
||||
onSelect={() => {
|
||||
const shape = editor.getShape(frame.id)
|
||||
if (shape) {
|
||||
editor.zoomToBounds(editor.getShapePageBounds(shape)!, {
|
||||
animation: { duration: 400, easing: (t) => t * (2 - t) },
|
||||
})
|
||||
editor.select(frame.id)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</TldrawUiMenuSubmenu>
|
||||
</TldrawUiMenuGroup>
|
||||
|
||||
{/* TODO: FIX & IMPLEMENT BROADCASTING*/}
|
||||
{/* <TldrawUiMenuGroup id="broadcast-controls">
|
||||
<TldrawUiMenuItem
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ export const overrides: TLUiOverrides = {
|
|||
icon: "prompt",
|
||||
label: "Prompt",
|
||||
type: "Prompt",
|
||||
kbd: "alt+p",
|
||||
kbd: "alt+l",
|
||||
readonlyOk: true,
|
||||
onSelect: () => editor.setCurrentTool("Prompt"),
|
||||
},
|
||||
|
|
@ -222,7 +222,7 @@ export const overrides: TLUiOverrides = {
|
|||
saveToPdf: {
|
||||
id: "save-to-pdf",
|
||||
label: "Save Selection as PDF",
|
||||
kbd: "alt+s",
|
||||
kbd: "alt+p",
|
||||
onSelect: () => {
|
||||
if (editor.getSelectedShapeIds().length > 0) {
|
||||
saveToPdf(editor)
|
||||
|
|
@ -348,66 +348,67 @@ export const overrides: TLUiOverrides = {
|
|||
}
|
||||
},
|
||||
},
|
||||
"next-slide": {
|
||||
id: "next-slide",
|
||||
label: "Next slide",
|
||||
kbd: "right",
|
||||
onSelect() {
|
||||
const slides = editor
|
||||
.getCurrentPageShapes()
|
||||
.filter((shape) => shape.type === "Slide")
|
||||
if (slides.length === 0) return
|
||||
//TODO: FIX PREV & NEXT SLIDE KEYBOARD COMMANDS
|
||||
// "next-slide": {
|
||||
// id: "next-slide",
|
||||
// label: "Next slide",
|
||||
// kbd: "right",
|
||||
// onSelect() {
|
||||
// const slides = editor
|
||||
// .getCurrentPageShapes()
|
||||
// .filter((shape) => shape.type === "Slide")
|
||||
// if (slides.length === 0) return
|
||||
|
||||
const currentSlide = editor
|
||||
.getSelectedShapes()
|
||||
.find((shape) => shape.type === "Slide")
|
||||
const currentIndex = currentSlide
|
||||
? slides.findIndex((slide) => slide.id === currentSlide.id)
|
||||
: -1
|
||||
// const currentSlide = editor
|
||||
// .getSelectedShapes()
|
||||
// .find((shape) => shape.type === "Slide")
|
||||
// const currentIndex = currentSlide
|
||||
// ? slides.findIndex((slide) => slide.id === currentSlide.id)
|
||||
// : -1
|
||||
|
||||
// Calculate next index with wraparound
|
||||
const nextIndex =
|
||||
currentIndex === -1
|
||||
? 0
|
||||
: currentIndex >= slides.length - 1
|
||||
? 0
|
||||
: currentIndex + 1
|
||||
// // Calculate next index with wraparound
|
||||
// const nextIndex =
|
||||
// currentIndex === -1
|
||||
// ? 0
|
||||
// : currentIndex >= slides.length - 1
|
||||
// ? 0
|
||||
// : currentIndex + 1
|
||||
|
||||
const nextSlide = slides[nextIndex]
|
||||
// const nextSlide = slides[nextIndex]
|
||||
|
||||
editor.select(nextSlide.id)
|
||||
editor.stopCameraAnimation()
|
||||
moveToSlide(editor, nextSlide as ISlideShape)
|
||||
},
|
||||
},
|
||||
"previous-slide": {
|
||||
id: "previous-slide",
|
||||
label: "Previous slide",
|
||||
kbd: "left",
|
||||
onSelect() {
|
||||
const slides = editor
|
||||
.getCurrentPageShapes()
|
||||
.filter((shape) => shape.type === "Slide")
|
||||
if (slides.length === 0) return
|
||||
// editor.select(nextSlide.id)
|
||||
// editor.stopCameraAnimation()
|
||||
// moveToSlide(editor, nextSlide as ISlideShape)
|
||||
// },
|
||||
// },
|
||||
// "previous-slide": {
|
||||
// id: "previous-slide",
|
||||
// label: "Previous slide",
|
||||
// kbd: "left",
|
||||
// onSelect() {
|
||||
// const slides = editor
|
||||
// .getCurrentPageShapes()
|
||||
// .filter((shape) => shape.type === "Slide")
|
||||
// if (slides.length === 0) return
|
||||
|
||||
const currentSlide = editor
|
||||
.getSelectedShapes()
|
||||
.find((shape) => shape.type === "Slide")
|
||||
const currentIndex = currentSlide
|
||||
? slides.findIndex((slide) => slide.id === currentSlide.id)
|
||||
: -1
|
||||
// const currentSlide = editor
|
||||
// .getSelectedShapes()
|
||||
// .find((shape) => shape.type === "Slide")
|
||||
// const currentIndex = currentSlide
|
||||
// ? slides.findIndex((slide) => slide.id === currentSlide.id)
|
||||
// : -1
|
||||
|
||||
// Calculate previous index with wraparound
|
||||
const previousIndex =
|
||||
currentIndex <= 0 ? slides.length - 1 : currentIndex - 1
|
||||
// // Calculate previous index with wraparound
|
||||
// const previousIndex =
|
||||
// currentIndex <= 0 ? slides.length - 1 : currentIndex - 1
|
||||
|
||||
const previousSlide = slides[previousIndex]
|
||||
// const previousSlide = slides[previousIndex]
|
||||
|
||||
editor.select(previousSlide.id)
|
||||
editor.stopCameraAnimation()
|
||||
moveToSlide(editor, previousSlide as ISlideShape)
|
||||
},
|
||||
},
|
||||
// editor.select(previousSlide.id)
|
||||
// editor.stopCameraAnimation()
|
||||
// moveToSlide(editor, previousSlide as ISlideShape)
|
||||
// },
|
||||
// },
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,17 @@ function getAssetObjectName(uploadId: string) {
|
|||
|
||||
// when a user uploads an asset, we store it in the bucket. we only allow image and video assets.
|
||||
export async function handleAssetUpload(request: IRequest, env: Environment) {
|
||||
// If this is a preflight request, return appropriate CORS headers
|
||||
if (request.method === 'OPTIONS') {
|
||||
const headers = new Headers({
|
||||
'access-control-allow-origin': '*',
|
||||
'access-control-allow-methods': 'POST, OPTIONS',
|
||||
'access-control-allow-headers': 'content-type',
|
||||
'access-control-max-age': '86400',
|
||||
})
|
||||
return new Response(null, { headers })
|
||||
}
|
||||
|
||||
try {
|
||||
const objectName = getAssetObjectName(request.params.uploadId)
|
||||
|
||||
|
|
@ -67,10 +78,14 @@ export async function handleAssetDownload(
|
|||
headers.set('cache-control', 'public, max-age=31536000, immutable')
|
||||
headers.set('etag', object.httpEtag)
|
||||
|
||||
// we set CORS headers so all clients can access assets. we do this here so our `cors` helper in
|
||||
// worker.ts doesn't try to set extra cors headers on responses that have been read from the
|
||||
// cache, which isn't allowed by cloudflare.
|
||||
// Set comprehensive CORS headers for asset access
|
||||
headers.set('access-control-allow-origin', '*')
|
||||
headers.set('access-control-allow-methods', 'GET, HEAD, OPTIONS')
|
||||
headers.set('access-control-allow-headers', '*')
|
||||
headers.set('access-control-expose-headers', 'content-length, content-range')
|
||||
headers.set('cross-origin-resource-policy', 'cross-origin')
|
||||
headers.set('cross-origin-opener-policy', 'same-origin')
|
||||
headers.set('cross-origin-embedder-policy', 'require-corp')
|
||||
|
||||
// cloudflare doesn't set the content-range header automatically in writeHttpMetadata, so we
|
||||
// need to do it ourselves.
|
||||
|
|
|
|||
168
worker/worker.ts
168
worker/worker.ts
|
|
@ -58,6 +58,11 @@ const { preflight, corsify } = cors({
|
|||
"Sec-WebSocket-Version",
|
||||
"Sec-WebSocket-Extensions",
|
||||
"Sec-WebSocket-Protocol",
|
||||
"Content-Length",
|
||||
"Content-Range",
|
||||
"Range",
|
||||
"If-None-Match",
|
||||
"If-Modified-Since"
|
||||
],
|
||||
maxAge: 86400,
|
||||
credentials: true,
|
||||
|
|
@ -155,6 +160,169 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
|||
}
|
||||
})
|
||||
|
||||
// Add new transcription endpoints
|
||||
.post("/daily/rooms/:roomName/start-transcription", async (req) => {
|
||||
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
||||
const { roomName } = 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/rooms/${roomName}/transcription/start`, {
|
||||
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' }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.post("/daily/rooms/:roomName/stop-transcription", async (req) => {
|
||||
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
||||
const { roomName } = 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/rooms/${roomName}/transcription/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' }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Add endpoint to get transcript access link
|
||||
.get("/daily/transcript/:transcriptId/access-link", async (req) => {
|
||||
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
||||
const { transcriptId } = 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/transcript/${transcriptId}/access-link`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
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 endpoint to get transcript text
|
||||
.get("/daily/transcript/:transcriptId", async (req) => {
|
||||
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
||||
const { transcriptId } = 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/transcripts/${transcriptId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
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