updated website copy, installed locked-view function (coordinates break when locked tho), trying to get video transcripts working
This commit is contained in:
parent
2a3b79df15
commit
d7b1e348e9
10
index.html
10
index.html
|
|
@ -13,21 +13,21 @@
|
||||||
|
|
||||||
<!-- Social Meta Tags -->
|
<!-- Social Meta Tags -->
|
||||||
<meta name="description"
|
<meta name="description"
|
||||||
content="My research investigates the intersection of computing, human-system interfaces, and emancipatory politics. I am interested in the potential of computing as a medium for thought, as a tool for collective action, and as a means of emancipation.">
|
content="Mycelial experimentation in the digital realm.">
|
||||||
|
|
||||||
<meta property="og:url" content="https://jeffemmett.com">
|
<meta property="og:url" content="https://jeffemmett.com">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:title" content="Jeff Emmett">
|
<meta property="og:title" content="A MycoPunk Website">
|
||||||
<meta property="og:description"
|
<meta property="og:description"
|
||||||
content="My research doesn't investigate the intersection of computing, human-system interfaces, and emancipatory politics. I am interested in the potential of computing as a medium for thought, as a tool for collective action, and as a means of emancipation.">
|
content="Mycelial knowledge and economic experimentation in the digital realm.">
|
||||||
<meta property="og:image" content="/website-embed.png">
|
<meta property="og:image" content="/website-embed.png">
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<meta property="twitter:domain" content="jeffemmett.com">
|
<meta property="twitter:domain" content="jeffemmett.com">
|
||||||
<meta property="twitter:url" content="https://jeffemmett.com">
|
<meta property="twitter:url" content="https://jeffemmett.com">
|
||||||
<meta name="twitter:title" content="Jeff Emmett">
|
<meta name="twitter:title" content="A MycoPunk Website">
|
||||||
<meta name="twitter:description"
|
<meta name="twitter:description"
|
||||||
content="My research doesn't investigate the intersection of computing, human-system interfaces, and emancipatory politics. I am interested in the potential of computing as a medium for thought, as a tool for collective action, and as a means of emancipation.">
|
content="Mycelial knowledge and economic experimentation in the digital realm.">
|
||||||
<meta name="twitter:image" content="/website-embed.png">
|
<meta name="twitter:image" content="/website-embed.png">
|
||||||
|
|
||||||
<!-- Analytics -->
|
<!-- Analytics -->
|
||||||
|
|
|
||||||
|
|
@ -64,9 +64,9 @@ export function useCameraControls(editor: Editor | null) {
|
||||||
if (!editor) return
|
if (!editor) return
|
||||||
const camera = editor.getCamera()
|
const camera = editor.getCamera()
|
||||||
const url = new URL(window.location.href)
|
const url = new URL(window.location.href)
|
||||||
url.searchParams.set("x", Math.round(camera.x).toString())
|
url.searchParams.set("x", camera.x.toFixed(2))
|
||||||
url.searchParams.set("y", Math.round(camera.y).toString())
|
url.searchParams.set("y", camera.y.toFixed(2))
|
||||||
url.searchParams.set("zoom", Math.round(camera.z).toString())
|
url.searchParams.set("zoom", camera.z.toFixed(2))
|
||||||
navigator.clipboard.writeText(url.toString())
|
navigator.clipboard.writeText(url.toString())
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,10 @@ import { llm } from "@/utils/llmUtils"
|
||||||
import {
|
import {
|
||||||
lockElement,
|
lockElement,
|
||||||
unlockElement,
|
unlockElement,
|
||||||
setInitialCameraFromUrl,
|
//setInitialCameraFromUrl,
|
||||||
initLockIndicators,
|
initLockIndicators,
|
||||||
watchForLockedShapes,
|
watchForLockedShapes,
|
||||||
|
zoomToSelection,
|
||||||
} from "@/ui/cameraUtils"
|
} from "@/ui/cameraUtils"
|
||||||
|
|
||||||
// Default to production URL if env var isn't available
|
// Default to production URL if env var isn't available
|
||||||
|
|
@ -77,6 +78,8 @@ export function Board() {
|
||||||
const store = useSync(storeConfig)
|
const store = useSync(storeConfig)
|
||||||
const [editor, setEditor] = useState<Editor | null>(null)
|
const [editor, setEditor] = useState<Editor | null>(null)
|
||||||
|
|
||||||
|
const [isCameraLocked, setIsCameraLocked] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const value = localStorage.getItem("makereal_settings_2")
|
const value = localStorage.getItem("makereal_settings_2")
|
||||||
if (value) {
|
if (value) {
|
||||||
|
|
@ -97,6 +100,71 @@ export function Board() {
|
||||||
watchForLockedShapes(editor)
|
watchForLockedShapes(editor)
|
||||||
}, [editor])
|
}, [editor])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editor) return
|
||||||
|
|
||||||
|
// First set the camera position
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
const x = url.searchParams.get("x")
|
||||||
|
const y = url.searchParams.get("y")
|
||||||
|
const zoom = url.searchParams.get("zoom")
|
||||||
|
const shapeId = url.searchParams.get("shapeId")
|
||||||
|
const frameId = url.searchParams.get("frameId")
|
||||||
|
const isLocked = url.searchParams.get("isLocked") === "true"
|
||||||
|
|
||||||
|
const initializeCamera = async () => {
|
||||||
|
// Start with camera unlocked
|
||||||
|
setIsCameraLocked(false)
|
||||||
|
|
||||||
|
if (x && y && zoom) {
|
||||||
|
editor.stopCameraAnimation()
|
||||||
|
|
||||||
|
// Set camera position immediately when editor is available
|
||||||
|
editor.setCamera(
|
||||||
|
{
|
||||||
|
x: parseFloat(parseFloat(x).toFixed(2)),
|
||||||
|
y: parseFloat(parseFloat(y).toFixed(2)),
|
||||||
|
z: parseFloat(parseFloat(zoom).toFixed(2))
|
||||||
|
},
|
||||||
|
{ animation: { duration: 0 } }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure camera update is applied
|
||||||
|
editor.updateInstanceState({ ...editor.getInstanceState() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle shape/frame selection after camera position is set
|
||||||
|
if (shapeId) {
|
||||||
|
editor.select(shapeId as TLShapeId)
|
||||||
|
const bounds = editor.getSelectionPageBounds()
|
||||||
|
if (bounds && !x && !y && !zoom) {
|
||||||
|
zoomToSelection(editor)
|
||||||
|
}
|
||||||
|
} else if (frameId) {
|
||||||
|
editor.select(frameId as TLShapeId)
|
||||||
|
const frame = editor.getShape(frameId as TLShapeId)
|
||||||
|
if (frame && !x && !y && !zoom) {
|
||||||
|
const bounds = editor.getShapePageBounds(frame)
|
||||||
|
if (bounds) {
|
||||||
|
editor.zoomToBounds(bounds, {
|
||||||
|
targetZoom: 1,
|
||||||
|
animation: { duration: 0 },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock camera after all initialization is complete
|
||||||
|
if (isLocked) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setIsCameraLocked(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeCamera()
|
||||||
|
}, [editor])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "fixed", inset: 0 }}>
|
<div style={{ position: "fixed", inset: 0 }}>
|
||||||
<Tldraw
|
<Tldraw
|
||||||
|
|
@ -115,6 +183,7 @@ export function Board() {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
cameraOptions={{
|
cameraOptions={{
|
||||||
|
isLocked: isCameraLocked,
|
||||||
zoomSteps: [
|
zoomSteps: [
|
||||||
0.001, // Min zoom
|
0.001, // Min zoom
|
||||||
0.0025,
|
0.0025,
|
||||||
|
|
@ -138,8 +207,7 @@ export function Board() {
|
||||||
setEditor(editor)
|
setEditor(editor)
|
||||||
editor.registerExternalAssetHandler("url", unfurlBookmarkUrl)
|
editor.registerExternalAssetHandler("url", unfurlBookmarkUrl)
|
||||||
editor.setCurrentTool("hand")
|
editor.setCurrentTool("hand")
|
||||||
setInitialCameraFromUrl(editor)
|
|
||||||
handleInitialPageLoad(editor)
|
|
||||||
registerPropagators(editor, [
|
registerPropagators(editor, [
|
||||||
TickPropagator,
|
TickPropagator,
|
||||||
ChangePropagator,
|
ChangePropagator,
|
||||||
|
|
|
||||||
|
|
@ -4,34 +4,38 @@ export function Default() {
|
||||||
<header>Jeff Emmett</header>
|
<header>Jeff Emmett</header>
|
||||||
<h2>Hello! 👋🍄</h2>
|
<h2>Hello! 👋🍄</h2>
|
||||||
<p>
|
<p>
|
||||||
My research investigates the intersection of mycelium and emancipatory
|
My research investigates the intersection of mycelial patterns and emancipatory
|
||||||
technologies. I am interested in the potential of new convivial tooling
|
technologies. I am interested in the potential of new convivial tooling
|
||||||
as a medium for group consensus building and collective action, in order
|
as a medium for group consensus building and collective action, in order
|
||||||
to empower communities of practice to address their own challenges.
|
to empower communities of practice to address their local challenges in an
|
||||||
|
age of ecological and instititutional collapse.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
My current focus is basic research into the nature of digital
|
I let my curiosity about mushrooms guide me, taking inspiration from their
|
||||||
organisation, developing prototype toolkits to improve shared
|
willingness to playfully experiment and adapt, even in the most chaotic environments.
|
||||||
infrastructure, and applying this research to the design of new systems
|
I am fascinated by the potential of mycelial networks to create new forms of bottoms-up
|
||||||
and protocols which support the self-organisation of knowledge and
|
sensing, collective cohereing around sensible directions, and emergent dynamic action
|
||||||
emergent response to local needs.
|
towards addressing local challenges.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>My work</h2>
|
<h2>My work</h2>
|
||||||
<p>
|
<p>
|
||||||
Alongside my independent work, I am a researcher and engineering
|
I am fortunate enough to collaborate with some pretty incredible groups of
|
||||||
communicator at <a href="https://block.science/">Block Science</a>, an
|
researchers and builders. I am a research communicator at
|
||||||
advisor to the Active Inference Lab, Commons Stack, and the Trusted
|
<a href="https://block.science/">Block Science</a>, an
|
||||||
Seed. I am also an occasional collaborator with{" "}
|
advisor to the <a href= "https://activeinference.org/">Active Inference Lab</a>,
|
||||||
<a href="https://economicspace.agency/">ECSA</a>.
|
co-founder of <a href="https://commonsstack.org/">Commons Stack</a>, and
|
||||||
|
board member of the <a href="https://trustedseed.org/">Trusted Seed</a>. I am also
|
||||||
|
a collaborator with <a href="https://economicspace.agency/">The Economic Space Agency</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Get in touch</h2>
|
<h2>Get in Touch to Collaborate</h2>
|
||||||
<p>
|
<p>
|
||||||
I am on Twitter <a href="https://twitter.com/jeffemmett">@jeffemmett</a>
|
I am on Substack <a href="https://allthingsdecent.substack.com/">@All Things Decent</a>,
|
||||||
, Mastodon{" "}
|
Bluesky <a href="https://bsky.app/profile/jeffemmett.com">@jeffemmett</a>,
|
||||||
<a href="https://social.coop/@jeffemmett">@jeffemmett@social.coop</a>{" "}
|
Twitter <a href="https://x.com/jeffemmett">@jeffemmett</a>,
|
||||||
|
Mastodon<a href="https://social.coop/@jeffemmett">@jeffemmett@social.coop</a>
|
||||||
and GitHub <a href="https://github.com/Jeff-Emmett">@Jeff-Emmett</a>.
|
and GitHub <a href="https://github.com/Jeff-Emmett">@Jeff-Emmett</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
@ -42,34 +46,29 @@ export function Default() {
|
||||||
<li>
|
<li>
|
||||||
<a href="https://www.teamhuman.fm/episodes/238-jeff-emmett">
|
<a href="https://www.teamhuman.fm/episodes/238-jeff-emmett">
|
||||||
MycoPunk Futures on Team Human with Douglas Rushkoff
|
MycoPunk Futures on Team Human with Douglas Rushkoff
|
||||||
</a>{" "}
|
</a>
|
||||||
(<a href="artifact/tft-rocks-integration-domain.pdf">slides</a>)
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://www.youtube.com/watch?v=AFJFDajuCSg">
|
<a href="https://www.youtube.com/watch?v=AFJFDajuCSg">
|
||||||
Exploring MycoFi on the Greenpill Network with Kevin Owocki
|
Exploring MycoFi on the Greenpill Network with Kevin Owocki
|
||||||
</a>{" "}
|
</a>
|
||||||
(<a href="artifact/tft-rocks-integration-domain.pdf">slides</a>)
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://youtu.be/9ad2EJhMbZ8">
|
<a href="https://youtu.be/9ad2EJhMbZ8">
|
||||||
Re-imagining Human Value on the Telos Podcast with Rieki &
|
Re-imagining Human Value on the Telos Podcast with Rieki &
|
||||||
Brandonfrom SEEDS
|
Brandon from SEEDS
|
||||||
</a>{" "}
|
</a>
|
||||||
(<a href="artifact/tft-rocks-integration-domain.pdf">slides</a>)
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://www.youtube.com/watch?v=i8qcg7FfpLM&t=1348s">
|
<a href="https://www.youtube.com/watch?v=i8qcg7FfpLM&t=1348s">
|
||||||
Move Slow & Fix Things: Design Patterns from Nature
|
Move Slow & Fix Things: Design Patterns from Nature
|
||||||
</a>{" "}
|
</a>
|
||||||
(<a href="artifact/tft-rocks-integration-domain.pdf">slides</a>)
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://podcasters.spotify.com/pod/show/theownershipeconomy/episodes/Episode-009---Localized-Democracy-and-Public-Goods-with-Token-Engineering--with-Jeff-Emmett-of-The-Commons-Stack--BlockScience-Labs-e1ggkqo">
|
<a href="https://podcasters.spotify.com/pod/show/theownershipeconomy/episodes/Episode-009---Localized-Democracy-and-Public-Goods-with-Token-Engineering--with-Jeff-Emmett-of-The-Commons-Stack--BlockScience-Labs-e1ggkqo">
|
||||||
Localized Democracy and Public Goods with Token Engineering on the
|
Localized Democracy and Public Goods with Token Engineering on the
|
||||||
Ownership Economy
|
Ownership Economy
|
||||||
</a>{" "}
|
</a>
|
||||||
(<a href="artifact/tft-rocks-integration-domain.pdf">slides</a>)
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://youtu.be/kxcat-XBWas">
|
<a href="https://youtu.be/kxcat-XBWas">
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,11 @@ interface DailyApiResponse {
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DailyRecordingResponse {
|
interface DailyTranscriptResponse {
|
||||||
id: string;
|
id: string;
|
||||||
|
transcriptionId: string;
|
||||||
|
text?: string;
|
||||||
|
link?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IVideoChatShape = TLBaseShape<
|
export type IVideoChatShape = TLBaseShape<
|
||||||
|
|
@ -17,8 +20,9 @@ export type IVideoChatShape = TLBaseShape<
|
||||||
roomUrl: string | null
|
roomUrl: string | null
|
||||||
allowCamera: boolean
|
allowCamera: boolean
|
||||||
allowMicrophone: boolean
|
allowMicrophone: boolean
|
||||||
enableRecording: boolean
|
enableTranscription: boolean
|
||||||
recordingId: string | null // Track active recording
|
transcriptionId: string | null
|
||||||
|
isTranscribing: boolean
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
@ -36,8 +40,9 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
h: 600,
|
h: 600,
|
||||||
allowCamera: false,
|
allowCamera: false,
|
||||||
allowMicrophone: false,
|
allowMicrophone: false,
|
||||||
enableRecording: true,
|
enableTranscription: true,
|
||||||
recordingId: null
|
transcriptionId: null,
|
||||||
|
isTranscribing: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,80 +150,196 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async startRecording(shape: IVideoChatShape) {
|
async startTranscription(shape: IVideoChatShape) {
|
||||||
if (!shape.props.roomUrl) return;
|
if (!shape.props.roomUrl) return;
|
||||||
|
|
||||||
const workerUrl = import.meta.env.VITE_TLDRAW_WORKER_URL;
|
const workerUrl = import.meta.env.VITE_TLDRAW_WORKER_URL;
|
||||||
const apiKey = import.meta.env.VITE_DAILY_API_KEY;
|
const apiKey = import.meta.env.VITE_DAILY_API_KEY;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error('Daily.co API key not configured');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${workerUrl}/daily/recordings/start`, {
|
// Extract room name from the room URL
|
||||||
|
const roomName = new URL(shape.props.roomUrl).pathname.split('/').pop();
|
||||||
|
|
||||||
|
const response = await fetch(`${workerUrl}/daily/rooms/${roomName}/start-transcription`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${apiKey}`,
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
}
|
||||||
body: JSON.stringify({
|
|
||||||
room_name: shape.id,
|
|
||||||
layout: {
|
|
||||||
preset: "active-speaker"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Failed to start recording');
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(`Failed to start transcription: ${JSON.stringify(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json() as DailyRecordingResponse;
|
const data = await response.json() as DailyTranscriptResponse;
|
||||||
|
|
||||||
await this.editor.updateShape<IVideoChatShape>({
|
await this.editor.updateShape<IVideoChatShape>({
|
||||||
id: shape.id,
|
id: shape.id,
|
||||||
type: shape.type,
|
type: shape.type,
|
||||||
props: {
|
props: {
|
||||||
...shape.props,
|
...shape.props,
|
||||||
recordingId: data.id
|
transcriptionId: data.transcriptionId || data.id,
|
||||||
|
isTranscribing: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error starting recording:', error);
|
console.error('Error starting transcription:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopRecording(shape: IVideoChatShape) {
|
async stopTranscription(shape: IVideoChatShape) {
|
||||||
if (!shape.props.recordingId) return;
|
if (!shape.props.roomUrl) return;
|
||||||
|
|
||||||
const workerUrl = import.meta.env.VITE_TLDRAW_WORKER_URL;
|
const workerUrl = import.meta.env.VITE_TLDRAW_WORKER_URL;
|
||||||
const apiKey = import.meta.env.VITE_DAILY_API_KEY;
|
const apiKey = import.meta.env.VITE_DAILY_API_KEY;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error('Daily.co API key not configured');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetch(`${workerUrl}/daily/recordings/${shape.props.recordingId}/stop`, {
|
// Extract room name from the room URL
|
||||||
|
const roomName = new URL(shape.props.roomUrl).pathname.split('/').pop();
|
||||||
|
|
||||||
|
const response = await fetch(`${workerUrl}/daily/rooms/${roomName}/stop-transcription`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${apiKey}`
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(`Failed to stop transcription: ${JSON.stringify(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json() as DailyTranscriptResponse;
|
||||||
|
console.log('Stop transcription response:', data);
|
||||||
|
|
||||||
|
// Update both transcriptionId and isTranscribing state
|
||||||
await this.editor.updateShape<IVideoChatShape>({
|
await this.editor.updateShape<IVideoChatShape>({
|
||||||
id: shape.id,
|
id: shape.id,
|
||||||
type: shape.type,
|
type: shape.type,
|
||||||
props: {
|
props: {
|
||||||
...shape.props,
|
...shape.props,
|
||||||
recordingId: null
|
transcriptionId: data.transcriptionId || data.id || 'completed',
|
||||||
|
isTranscribing: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error stopping recording:', error);
|
console.error('Error stopping transcription:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTranscriptionText(transcriptId: string): Promise<string> {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Fetching transcript for ID:', transcriptId); // Debug log
|
||||||
|
|
||||||
|
const response = await fetch(`${workerUrl}/transcript/${transcriptId}`, { // Remove 'daily' from path
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
console.error('Transcript API response:', error); // Debug log
|
||||||
|
throw new Error(`Failed to get transcription: ${JSON.stringify(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json() as DailyTranscriptResponse;
|
||||||
|
console.log('Transcript data received:', data); // Debug log
|
||||||
|
return data.text || 'No transcription available';
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTranscriptAccessLink(transcriptId: string): Promise<string> {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Fetching transcript access link for ID:', transcriptId); // Debug log
|
||||||
|
|
||||||
|
const response = await fetch(`${workerUrl}/transcript/${transcriptId}/access-link`, { // Remove 'daily' from path
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
console.error('Transcript link API response:', error); // Debug log
|
||||||
|
throw new Error(`Failed to get transcript access link: ${JSON.stringify(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json() as DailyTranscriptResponse;
|
||||||
|
console.log('Transcript link data received:', data); // Debug log
|
||||||
|
return data.link || 'No transcript link available';
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [roomUrl, setRoomUrl] = useState<string | null>(shape.props.roomUrl)
|
const [roomUrl, setRoomUrl] = useState<string | null>(shape.props.roomUrl)
|
||||||
|
const [isCallActive, setIsCallActive] = useState(false)
|
||||||
|
|
||||||
|
const handleIframeMessage = (event: MessageEvent) => {
|
||||||
|
// Check if message is from Daily.co
|
||||||
|
if (!event.origin.includes('daily.co')) return;
|
||||||
|
|
||||||
|
console.log('Daily message received:', event.data);
|
||||||
|
|
||||||
|
// Check for call state updates
|
||||||
|
if (event.data?.action === 'daily-method-result') {
|
||||||
|
// Handle join success
|
||||||
|
if (event.data.method === 'join' && !event.data.error) {
|
||||||
|
console.log('Join successful - setting call as active');
|
||||||
|
setIsCallActive(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check for participant events
|
||||||
|
if (event.data?.action === 'participant-joined') {
|
||||||
|
console.log('Participant joined - setting call as active');
|
||||||
|
setIsCallActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for call ended
|
||||||
|
if (event.data?.action === 'left-meeting' ||
|
||||||
|
event.data?.action === 'participant-left') {
|
||||||
|
console.log('Call ended - setting call as inactive');
|
||||||
|
setIsCallActive(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('message', handleIframeMessage);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('message', handleIframeMessage);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
|
|
@ -250,7 +371,7 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
return () => {
|
return () => {
|
||||||
mounted = false;
|
mounted = false;
|
||||||
};
|
};
|
||||||
}, [shape.id]); // Only re-run if shape.id changes
|
}, [shape.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
|
|
@ -282,6 +403,28 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
}
|
}
|
||||||
}, [shape.props.allowCamera, shape.props.allowMicrophone])
|
}, [shape.props.allowCamera, shape.props.allowMicrophone])
|
||||||
|
|
||||||
|
const handleTranscriptionClick = async (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (!isCallActive) {
|
||||||
|
console.log('Cannot control transcription when call is not active');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (shape.props.isTranscribing) {
|
||||||
|
console.log('Stopping transcription');
|
||||||
|
await this.stopTranscription(shape);
|
||||||
|
} else {
|
||||||
|
console.log('Starting transcription');
|
||||||
|
await this.startTranscription(shape);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Transcription error:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <div>Error creating room: {error.message}</div>
|
return <div>Error creating room: {error.message}</div>
|
||||||
}
|
}
|
||||||
|
|
@ -317,6 +460,16 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
|
|
||||||
console.log(roomUrl)
|
console.log(roomUrl)
|
||||||
|
|
||||||
|
// Debug log for render
|
||||||
|
console.log('Current call state:', { isCallActive, roomUrl });
|
||||||
|
|
||||||
|
// Add debug log before render
|
||||||
|
console.log('Rendering component with states:', {
|
||||||
|
isCallActive,
|
||||||
|
isTranscribing: shape.props.isTranscribing,
|
||||||
|
roomUrl
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -324,7 +477,7 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
height: `${shape.props.h}px`,
|
height: `${shape.props.h}px`,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
pointerEvents: "all",
|
pointerEvents: "all",
|
||||||
overflow: "hidden",
|
overflow: "visible",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<iframe
|
<iframe
|
||||||
|
|
@ -339,58 +492,80 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
}}
|
}}
|
||||||
allow={`camera ${shape.props.allowCamera ? "self" : ""}; microphone ${
|
allow="camera *; microphone *; display-capture *; clipboard-read; clipboard-write"
|
||||||
shape.props.allowMicrophone ? "self" : ""
|
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads allow-modals"
|
||||||
}`}
|
/>
|
||||||
></iframe>
|
|
||||||
|
|
||||||
{shape.props.enableRecording && (
|
{/* Add data-testid to help debug iframe messages */}
|
||||||
<button
|
<div data-testid="call-status">
|
||||||
onClick={async () => {
|
Call Active: {isCallActive ? 'Yes' : 'No'}
|
||||||
try {
|
</div>
|
||||||
if (shape.props.recordingId) {
|
|
||||||
await this.stopRecording(shape);
|
|
||||||
} else {
|
|
||||||
await this.startRecording(shape);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Recording error:', err);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "8px",
|
|
||||||
right: "8px",
|
|
||||||
padding: "4px 8px",
|
|
||||||
background: shape.props.recordingId ? "#ff4444" : "#ffffff",
|
|
||||||
border: "1px solid #ccc",
|
|
||||||
borderRadius: "4px",
|
|
||||||
cursor: "pointer",
|
|
||||||
zIndex: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{shape.props.recordingId ? "Stop Recording" : "Start Recording"}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<p
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
bottom: 0,
|
bottom: -48,
|
||||||
left: 0,
|
left: 0,
|
||||||
|
right: 0,
|
||||||
margin: "8px",
|
margin: "8px",
|
||||||
padding: "4px 8px",
|
padding: "8px 12px",
|
||||||
background: "rgba(255, 255, 255, 0.9)",
|
background: "rgba(255, 255, 255, 0.95)",
|
||||||
borderRadius: "4px",
|
borderRadius: "6px",
|
||||||
fontSize: "12px",
|
fontSize: "12px",
|
||||||
pointerEvents: "all",
|
pointerEvents: "all",
|
||||||
cursor: "text",
|
touchAction: "manipulation",
|
||||||
userSelect: "text",
|
display: "flex",
|
||||||
zIndex: 1,
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
zIndex: 999,
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
|
||||||
|
userSelect: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
url: {roomUrl}
|
<span style={{
|
||||||
</p>
|
cursor: "text",
|
||||||
|
userSelect: "text",
|
||||||
|
maxWidth: "60%",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
pointerEvents: "all",
|
||||||
|
touchAction: "auto"
|
||||||
|
}}>
|
||||||
|
url: {roomUrl}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={handleTranscriptionClick}
|
||||||
|
disabled={!isCallActive}
|
||||||
|
style={{
|
||||||
|
marginLeft: "12px",
|
||||||
|
padding: "6px 12px",
|
||||||
|
background: shape.props.isTranscribing ? "#ff4444" : "#ffffff",
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
borderRadius: "4px",
|
||||||
|
cursor: isCallActive ? "pointer" : "not-allowed",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
flexShrink: 0,
|
||||||
|
pointerEvents: isCallActive ? "all" : "none", // Add explicit pointer-events control
|
||||||
|
touchAction: "manipulation",
|
||||||
|
WebkitTapHighlightColor: "transparent",
|
||||||
|
userSelect: "none",
|
||||||
|
minHeight: "32px",
|
||||||
|
minWidth: "44px",
|
||||||
|
zIndex: 1000,
|
||||||
|
position: "relative",
|
||||||
|
opacity: isCallActive ? 1 : 0.5
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!isCallActive
|
||||||
|
? "Join call to enable transcription"
|
||||||
|
: shape.props.isTranscribing
|
||||||
|
? "Stop Transcription"
|
||||||
|
: "Start Transcription"
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { DefaultContextMenu, DefaultContextMenuContent } from "tldraw"
|
||||||
import { TLUiContextMenuProps, useEditor } from "tldraw"
|
import { TLUiContextMenuProps, useEditor } from "tldraw"
|
||||||
import {
|
import {
|
||||||
cameraHistory,
|
cameraHistory,
|
||||||
|
copyLinkToLockedView,
|
||||||
} from "./cameraUtils"
|
} from "./cameraUtils"
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { saveToPdf } from "../utils/pdfUtils"
|
import { saveToPdf } from "../utils/pdfUtils"
|
||||||
|
|
@ -95,11 +96,13 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
|
||||||
<TldrawUiMenuGroup id="camera-controls">
|
<TldrawUiMenuGroup id="camera-controls">
|
||||||
<TldrawUiMenuItem {...customActions.zoomToSelection} disabled={!hasSelection} />
|
<TldrawUiMenuItem {...customActions.zoomToSelection} disabled={!hasSelection} />
|
||||||
<TldrawUiMenuItem {...customActions.copyLinkToCurrentView} />
|
<TldrawUiMenuItem {...customActions.copyLinkToCurrentView} />
|
||||||
|
<TldrawUiMenuItem {...customActions.copyLockedLink} />
|
||||||
<TldrawUiMenuItem {...customActions.revertCamera} disabled={!hasCameraHistory} />
|
<TldrawUiMenuItem {...customActions.revertCamera} disabled={!hasCameraHistory} />
|
||||||
<TldrawUiMenuItem {...customActions.lockElement} disabled={!hasSelection} />
|
<TldrawUiMenuItem {...customActions.lockElement} disabled={!hasSelection} />
|
||||||
<TldrawUiMenuItem {...customActions.unlockElement} disabled={!hasSelection} />
|
<TldrawUiMenuItem {...customActions.unlockElement} disabled={!hasSelection} />
|
||||||
<TldrawUiMenuItem {...customActions.saveToPdf} disabled={!hasSelection} />
|
<TldrawUiMenuItem {...customActions.saveToPdf} disabled={!hasSelection} />
|
||||||
<TldrawUiMenuItem {...customActions.llm} disabled={!hasSelection} />
|
<TldrawUiMenuItem {...customActions.llm} disabled={!hasSelection} />
|
||||||
|
|
||||||
</TldrawUiMenuGroup>
|
</TldrawUiMenuGroup>
|
||||||
|
|
||||||
{/* Creation Tools Group */}
|
{/* Creation Tools Group */}
|
||||||
|
|
|
||||||
|
|
@ -81,9 +81,9 @@ export const zoomToSelection = (editor: Editor) => {
|
||||||
const newCamera = editor.getCamera()
|
const newCamera = editor.getCamera()
|
||||||
const url = new URL(window.location.href)
|
const url = new URL(window.location.href)
|
||||||
url.searchParams.set("shapeId", selectedIds[0].toString())
|
url.searchParams.set("shapeId", selectedIds[0].toString())
|
||||||
url.searchParams.set("x", Math.round(newCamera.x).toString())
|
url.searchParams.set("x", newCamera.x.toFixed(2))
|
||||||
url.searchParams.set("y", Math.round(newCamera.y).toString())
|
url.searchParams.set("y", newCamera.y.toFixed(2))
|
||||||
url.searchParams.set("zoom", Math.round(newCamera.z).toString())
|
url.searchParams.set("zoom", newCamera.z.toFixed(2))
|
||||||
window.history.replaceState(null, "", url.toString())
|
window.history.replaceState(null, "", url.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,41 +119,49 @@ export const revertCamera = (editor: Editor) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const copyLinkToCurrentView = async (editor: Editor) => {
|
export const copyLinkToCurrentView = async (editor: Editor) => {
|
||||||
|
if (!editor.store.serialize()) return
|
||||||
if (!editor.store.serialize()) {
|
|
||||||
//console.warn("Store not ready")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const baseUrl = `${window.location.origin}${window.location.pathname}`
|
const baseUrl = `${window.location.origin}${window.location.pathname}`
|
||||||
const url = new URL(baseUrl)
|
const url = new URL(baseUrl)
|
||||||
const camera = editor.getCamera()
|
const camera = editor.getCamera()
|
||||||
|
|
||||||
// Round camera values to integers
|
// Round camera values to 2 decimal places
|
||||||
url.searchParams.set("x", Math.round(camera.x).toString())
|
url.searchParams.set("x", camera.x.toFixed(2))
|
||||||
url.searchParams.set("y", Math.round(camera.y).toString())
|
url.searchParams.set("y", camera.y.toFixed(2))
|
||||||
url.searchParams.set("zoom", Math.round(camera.z).toString())
|
url.searchParams.set("zoom", camera.z.toFixed(2))
|
||||||
|
|
||||||
const selectedIds = editor.getSelectedShapeIds()
|
const selectedIds = editor.getSelectedShapeIds()
|
||||||
if (selectedIds.length > 0) {
|
if (selectedIds.length > 0) {
|
||||||
url.searchParams.set("shapeId", selectedIds[0].toString())
|
url.searchParams.set("shapeId", selectedIds[0].toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalUrl = url.toString()
|
await navigator.clipboard.writeText(url.toString())
|
||||||
|
} catch (error) {
|
||||||
|
alert("Failed to copy link. Please check clipboard permissions.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
export const copyLinkToLockedView = async (editor: Editor) => {
|
||||||
await navigator.clipboard.writeText(finalUrl)
|
if (!editor.store.serialize()) return
|
||||||
} else {
|
|
||||||
const textArea = document.createElement("textarea")
|
try {
|
||||||
textArea.value = finalUrl
|
const baseUrl = `${window.location.origin}${window.location.pathname}`
|
||||||
document.body.appendChild(textArea)
|
const url = new URL(baseUrl)
|
||||||
try {
|
const camera = editor.getCamera()
|
||||||
await navigator.clipboard.writeText(textArea.value)
|
|
||||||
} catch (err) {
|
// Round camera values to 2 decimal places
|
||||||
}
|
url.searchParams.set("x", camera.x.toFixed(2))
|
||||||
document.body.removeChild(textArea)
|
url.searchParams.set("y", camera.y.toFixed(2))
|
||||||
|
url.searchParams.set("zoom", camera.z.toFixed(2))
|
||||||
|
url.searchParams.set("isLocked", "true")
|
||||||
|
|
||||||
|
const selectedIds = editor.getSelectedShapeIds()
|
||||||
|
if (selectedIds.length > 0) {
|
||||||
|
url.searchParams.set("shapeId", selectedIds[0].toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(url.toString())
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert("Failed to copy link. Please check clipboard permissions.")
|
alert("Failed to copy link. Please check clipboard permissions.")
|
||||||
}
|
}
|
||||||
|
|
@ -283,47 +291,55 @@ export const initLockIndicators = (editor: Editor) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setInitialCameraFromUrl = (editor: Editor) => {
|
// export const setInitialCameraFromUrl = (editor: Editor) => {
|
||||||
const url = new URL(window.location.href)
|
// const url = new URL(window.location.href)
|
||||||
const x = url.searchParams.get("x")
|
// const x = url.searchParams.get("x")
|
||||||
const y = url.searchParams.get("y")
|
// const y = url.searchParams.get("y")
|
||||||
const zoom = url.searchParams.get("zoom")
|
// const zoom = url.searchParams.get("zoom")
|
||||||
const shapeId = url.searchParams.get("shapeId")
|
// const shapeId = url.searchParams.get("shapeId")
|
||||||
const frameId = url.searchParams.get("frameId")
|
// const frameId = url.searchParams.get("frameId")
|
||||||
|
// const isLocked = url.searchParams.get("isLocked") === "true"
|
||||||
|
|
||||||
if (x && y && zoom) {
|
// // Always set camera position first if coordinates exist
|
||||||
editor.stopCameraAnimation()
|
// if (x && y && zoom) {
|
||||||
editor.setCamera(
|
// editor.stopCameraAnimation()
|
||||||
{
|
// // Force camera position update
|
||||||
x: Math.round(parseFloat(x)),
|
// editor.setCamera(
|
||||||
y: Math.round(parseFloat(y)),
|
// {
|
||||||
z: Math.round(parseFloat(zoom))
|
// x: Math.round(parseFloat(x)),
|
||||||
},
|
// y: Math.round(parseFloat(y)),
|
||||||
{ animation: { duration: 0 } }
|
// z: Math.round(parseFloat(zoom))
|
||||||
)
|
// },
|
||||||
}
|
// { animation: { duration: 0 } }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// // Ensure camera update is applied
|
||||||
|
// editor.updateInstanceState({ ...editor.getInstanceState() })
|
||||||
|
// }
|
||||||
|
|
||||||
// Handle shape/frame selection and zoom
|
// // Handle other camera operations after position is set
|
||||||
if (shapeId) {
|
// if (shapeId) {
|
||||||
editor.select(shapeId as TLShapeId)
|
// editor.select(shapeId as TLShapeId)
|
||||||
const bounds = editor.getSelectionPageBounds()
|
// const bounds = editor.getSelectionPageBounds()
|
||||||
if (bounds && !x && !y && !zoom) {
|
// if (bounds && !x && !y && !zoom) {
|
||||||
zoomToSelection(editor)
|
// zoomToSelection(editor)
|
||||||
}
|
// }
|
||||||
} else if (frameId) {
|
// } else if (frameId) {
|
||||||
editor.select(frameId as TLShapeId)
|
// editor.select(frameId as TLShapeId)
|
||||||
const frame = editor.getShape(frameId as TLShapeId)
|
// const frame = editor.getShape(frameId as TLShapeId)
|
||||||
if (frame && !x && !y && !zoom) {
|
// if (frame && !x && !y && !zoom) {
|
||||||
const bounds = editor.getShapePageBounds(frame as TLShape)
|
// const bounds = editor.getShapePageBounds(frame as TLShape)
|
||||||
if (bounds) {
|
// if (bounds) {
|
||||||
editor.zoomToBounds(bounds, {
|
// editor.zoomToBounds(bounds, {
|
||||||
targetZoom: 1,
|
// targetZoom: 1,
|
||||||
animation: { duration: 0 },
|
// animation: { duration: 0 },
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
|
||||||
|
// return isLocked
|
||||||
|
// }
|
||||||
|
|
||||||
export const zoomToFrame = (editor: Editor, frameId: string) => {
|
export const zoomToFrame = (editor: Editor, frameId: string) => {
|
||||||
if (!editor) return
|
if (!editor) return
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
revertCamera,
|
revertCamera,
|
||||||
unlockElement,
|
unlockElement,
|
||||||
zoomToSelection,
|
zoomToSelection,
|
||||||
|
copyLinkToLockedView,
|
||||||
} from "./cameraUtils"
|
} from "./cameraUtils"
|
||||||
import { saveToPdf } from "../utils/pdfUtils"
|
import { saveToPdf } from "../utils/pdfUtils"
|
||||||
import { searchText } from "../utils/searchUtils"
|
import { searchText } from "../utils/searchUtils"
|
||||||
|
|
@ -348,6 +349,16 @@ export const overrides: TLUiOverrides = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
//TODO: FIX COPY LOCKED LINK
|
||||||
|
copyLockedLink: {
|
||||||
|
id: "copy-locked-link",
|
||||||
|
label: "Copy Locked View Link",
|
||||||
|
kbd: "alt+shift+c",
|
||||||
|
onSelect() {
|
||||||
|
copyLinkToLockedView(editor)
|
||||||
|
},
|
||||||
|
readonlyOk: true,
|
||||||
|
},
|
||||||
//TODO: FIX PREV & NEXT SLIDE KEYBOARD COMMANDS
|
//TODO: FIX PREV & NEXT SLIDE KEYBOARD COMMANDS
|
||||||
// "next-slide": {
|
// "next-slide": {
|
||||||
// id: "next-slide",
|
// id: "next-slide",
|
||||||
|
|
|
||||||
|
|
@ -77,9 +77,9 @@ export const searchText = (editor: Editor) => {
|
||||||
const newCamera = editor.getCamera()
|
const newCamera = editor.getCamera()
|
||||||
const url = new URL(window.location.href)
|
const url = new URL(window.location.href)
|
||||||
url.searchParams.set("shapeId", matchingShapes[0].id)
|
url.searchParams.set("shapeId", matchingShapes[0].id)
|
||||||
url.searchParams.set("x", newCamera.x.toString())
|
url.searchParams.set("x", newCamera.x.toFixed(2))
|
||||||
url.searchParams.set("y", newCamera.y.toString())
|
url.searchParams.set("y", newCamera.y.toFixed(2))
|
||||||
url.searchParams.set("zoom", newCamera.z.toString())
|
url.searchParams.set("zoom", newCamera.z.toFixed(2))
|
||||||
window.history.replaceState(null, "", url.toString())
|
window.history.replaceState(null, "", url.toString())
|
||||||
} else {
|
} else {
|
||||||
alert("No matches found")
|
alert("No matches found")
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,12 @@ export { TldrawDurableObject } from "./TldrawDurableObject"
|
||||||
// Define security headers
|
// Define security headers
|
||||||
const securityHeaders = {
|
const securityHeaders = {
|
||||||
"Content-Security-Policy":
|
"Content-Security-Policy":
|
||||||
"default-src 'self'; connect-src 'self' wss: https:; img-src 'self' data: blob: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';",
|
"default-src 'self'; connect-src 'self' wss: https:; img-src 'self' data: blob: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; frame-src 'self' https://*.daily.co; child-src 'self' https://*.daily.co;",
|
||||||
"X-Content-Type-Options": "nosniff",
|
"X-Content-Type-Options": "nosniff",
|
||||||
"X-Frame-Options": "DENY",
|
|
||||||
"X-XSS-Protection": "1; mode=block",
|
"X-XSS-Protection": "1; mode=block",
|
||||||
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
|
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
|
||||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||||
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
|
"Permissions-Policy": "camera=*, microphone=*, geolocation=()",
|
||||||
}
|
}
|
||||||
|
|
||||||
// we use itty-router (https://itty.dev/) to handle routing. in this example we turn on CORS because
|
// we use itty-router (https://itty.dev/) to handle routing. in this example we turn on CORS because
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue