Fixed asset upload CORS for broken links, updated markdown tool, changed keyboard shortcuts & menu ordering

This commit is contained in:
Jeff-Emmett 2025-03-19 17:24:22 -07:00
parent b9addbe417
commit 12d26d0643
7 changed files with 3041 additions and 324 deletions

2992
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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