291 lines
8.4 KiB
TypeScript
291 lines
8.4 KiB
TypeScript
import {
|
|
Editor,
|
|
TldrawUiMenuActionItem,
|
|
TldrawUiMenuItem,
|
|
TldrawUiMenuSubmenu,
|
|
TLGeoShape,
|
|
TLShape,
|
|
} from "tldraw"
|
|
import { TldrawUiMenuGroup } from "tldraw"
|
|
import { DefaultContextMenu } from "tldraw"
|
|
import { TLUiContextMenuProps, useEditor } from "tldraw"
|
|
import {
|
|
cameraHistory,
|
|
copyLinkToCurrentView,
|
|
lockCameraToFrame,
|
|
revertCamera,
|
|
zoomToSelection,
|
|
} from "./cameraUtils"
|
|
import { useState, useEffect } from "react"
|
|
import { saveToPdf } from "../utils/pdfUtils"
|
|
import { TLFrameShape } from "tldraw"
|
|
import { searchText } from "../utils/searchUtils"
|
|
import { llm } from "../utils/llmUtils"
|
|
import { getEdge } from "@/propagators/tlgraph"
|
|
|
|
const getAllFrames = (editor: Editor) => {
|
|
return editor
|
|
.getCurrentPageShapes()
|
|
.filter((shape): shape is TLFrameShape => shape.type === "frame")
|
|
.map((frame) => ({
|
|
id: frame.id,
|
|
title: frame.props.name || "Untitled Frame",
|
|
}))
|
|
}
|
|
|
|
export function CustomContextMenu(props: TLUiContextMenuProps) {
|
|
const editor = useEditor()
|
|
const [selectedShapes, setSelectedShapes] = useState<TLShape[]>([])
|
|
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
|
|
|
// Update selection state more frequently
|
|
useEffect(() => {
|
|
const updateSelection = () => {
|
|
setSelectedShapes(editor.getSelectedShapes())
|
|
setSelectedIds(editor.getSelectedShapeIds())
|
|
}
|
|
|
|
// Initial update
|
|
updateSelection()
|
|
|
|
// Subscribe to selection changes
|
|
const unsubscribe = editor.addListener("change", updateSelection)
|
|
|
|
return () => {
|
|
if (typeof unsubscribe === "function") {
|
|
;(unsubscribe as () => void)()
|
|
}
|
|
}
|
|
}, [editor])
|
|
|
|
const hasSelection = selectedIds.length > 0
|
|
const hasCameraHistory = cameraHistory.length > 0
|
|
|
|
// Check if exactly one frame is selected
|
|
const hasFrameSelected =
|
|
selectedShapes.length === 1 && selectedShapes[0].type === "frame"
|
|
|
|
return (
|
|
<DefaultContextMenu {...props}>
|
|
{/* Camera Controls Group */}
|
|
<TldrawUiMenuGroup id="camera-controls">
|
|
<TldrawUiMenuItem
|
|
id="zoom-to-selection"
|
|
label="Zoom to Selection"
|
|
icon="zoom-in"
|
|
kbd="z"
|
|
disabled={!hasSelection}
|
|
onSelect={() => zoomToSelection(editor)}
|
|
/>
|
|
<TldrawUiMenuItem
|
|
id="copy-link-to-current-view"
|
|
label="Copy Link to Current View"
|
|
icon="link"
|
|
kbd="alt+c"
|
|
onSelect={() => copyLinkToCurrentView(editor)}
|
|
/>
|
|
<TldrawUiMenuItem
|
|
id="revert-camera"
|
|
label="Revert Camera"
|
|
icon="undo"
|
|
kbd="alt+b"
|
|
disabled={!hasCameraHistory}
|
|
onSelect={() => revertCamera(editor)}
|
|
/>
|
|
<TldrawUiMenuItem
|
|
id="save-to-pdf"
|
|
label="Save Selection as PDF"
|
|
icon="file"
|
|
kbd="alt+p"
|
|
disabled={!hasSelection}
|
|
onSelect={() => saveToPdf(editor)}
|
|
/>
|
|
<TldrawUiMenuItem
|
|
id="run-llm-prompt"
|
|
label="Run LLM Prompt"
|
|
icon="file"
|
|
kbd="g"
|
|
disabled={!hasSelection}
|
|
onSelect={() => {
|
|
const selectedShape = editor.getSelectedShapes()[0];
|
|
if (!selectedShape || selectedShape.type !== 'arrow') return;
|
|
|
|
const edge = getEdge(selectedShape, editor);
|
|
if (!edge) return;
|
|
|
|
const sourceShape = editor.getShape(edge.from);
|
|
const sourceText =
|
|
sourceShape && sourceShape.type === "geo"
|
|
? (sourceShape as TLGeoShape).props.text
|
|
: "";
|
|
|
|
llm(
|
|
`Instruction: ${edge.text}
|
|
${sourceText ? `Context: ${sourceText}` : ""}`,
|
|
localStorage.getItem("openai_api_key") || "",
|
|
(partialResponse: string) => {
|
|
editor.updateShape({
|
|
id: edge.to,
|
|
type: "geo",
|
|
props: {
|
|
...(editor.getShape(edge.to) as TLGeoShape).props,
|
|
text: partialResponse
|
|
}
|
|
});
|
|
}
|
|
)
|
|
}}
|
|
/>
|
|
</TldrawUiMenuGroup>
|
|
|
|
{/* Creation Tools Group */}
|
|
<TldrawUiMenuGroup id="creation-tools">
|
|
<TldrawUiMenuItem
|
|
id="VideoChat"
|
|
label="Create Video Chat"
|
|
icon="video"
|
|
kbd="alt+v"
|
|
disabled={hasSelection}
|
|
onSelect={() => {
|
|
editor.setCurrentTool("VideoChat")
|
|
}}
|
|
/>
|
|
<TldrawUiMenuItem
|
|
id="ChatBox"
|
|
label="Create Chat Box"
|
|
icon="chat"
|
|
kbd="alt+c"
|
|
disabled={hasSelection}
|
|
onSelect={() => {
|
|
editor.setCurrentTool("ChatBox")
|
|
}}
|
|
/>
|
|
<TldrawUiMenuItem
|
|
id="Embed"
|
|
label="Create Embed"
|
|
icon="embed"
|
|
kbd="alt+e"
|
|
disabled={hasSelection}
|
|
onSelect={() => {
|
|
editor.setCurrentTool("Embed")
|
|
}}
|
|
/>
|
|
<TldrawUiMenuItem
|
|
id="Slide"
|
|
label="Create Slide"
|
|
icon="slides"
|
|
kbd="alt+s"
|
|
disabled={hasSelection}
|
|
onSelect={() => {
|
|
editor.setCurrentTool("Slide")
|
|
}}
|
|
/>
|
|
<TldrawUiMenuItem
|
|
id="MycrozineTemplate"
|
|
label="Create Mycrozine Template"
|
|
icon="rectangle"
|
|
kbd="m"
|
|
disabled={hasSelection}
|
|
onSelect={() => {
|
|
editor.setCurrentTool("MycrozineTemplate")
|
|
}}
|
|
/>
|
|
<TldrawUiMenuItem
|
|
id="Markdown"
|
|
label="Create Markdown"
|
|
icon="markdown"
|
|
kbd="alt+m"
|
|
disabled={hasSelection}
|
|
onSelect={() => {
|
|
editor.setCurrentTool("Markdown")
|
|
}}
|
|
/>
|
|
<TldrawUiMenuItem
|
|
id="Prompt"
|
|
label="Create Prompt"
|
|
icon="prompt"
|
|
kbd="alt+p"
|
|
disabled={hasSelection}
|
|
onSelect={() => {
|
|
editor.setCurrentTool("Prompt")
|
|
}}
|
|
/>
|
|
</TldrawUiMenuGroup>
|
|
|
|
{/* Frame Controls */}
|
|
<TldrawUiMenuGroup id="frame-controls">
|
|
<TldrawUiMenuItem
|
|
id="lock-to-frame"
|
|
label="Lock to Frame"
|
|
icon="lock"
|
|
kbd="shift+l"
|
|
disabled={!hasFrameSelected}
|
|
onSelect={() => lockCameraToFrame(editor)}
|
|
/>
|
|
</TldrawUiMenuGroup>
|
|
|
|
<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>
|
|
|
|
<TldrawUiMenuGroup id="broadcast-controls">
|
|
<TldrawUiMenuItem
|
|
id="broadcast-view"
|
|
label="Start Broadcasting View"
|
|
icon="broadcast"
|
|
kbd="alt+b"
|
|
onSelect={() => {
|
|
const otherUsers = Array.from(editor.store.allRecords()).filter(
|
|
(record) =>
|
|
record.typeName === "instance_presence" &&
|
|
record.id !== editor.user.getId(),
|
|
)
|
|
otherUsers.forEach((user) => editor.startFollowingUser(user.id))
|
|
}}
|
|
/>
|
|
<TldrawUiMenuItem
|
|
id="stop-broadcast"
|
|
label="Stop Broadcasting View"
|
|
icon="broadcast-off"
|
|
kbd="alt+shift+b"
|
|
onSelect={() => {
|
|
const otherUsers = Array.from(editor.store.allRecords()).filter(
|
|
(record) =>
|
|
record.typeName === "instance_presence" &&
|
|
record.id !== editor.user.getId(),
|
|
)
|
|
otherUsers.forEach((_user) => editor.stopFollowingUser())
|
|
}}
|
|
/>
|
|
</TldrawUiMenuGroup>
|
|
|
|
<TldrawUiMenuGroup id="search-controls">
|
|
<TldrawUiMenuItem
|
|
id="search-text"
|
|
label="Search Text"
|
|
icon="search"
|
|
kbd="s"
|
|
onSelect={() => searchText(editor)}
|
|
/>
|
|
</TldrawUiMenuGroup>
|
|
</DefaultContextMenu>
|
|
)
|
|
}
|