Clean up tool names
This commit is contained in:
parent
86b37b9cc8
commit
9b33efdcb3
|
|
@ -25,7 +25,7 @@ import {
|
||||||
ClickPropagator,
|
ClickPropagator,
|
||||||
} from "@/propagators/ScopedPropagators"
|
} from "@/propagators/ScopedPropagators"
|
||||||
import { SlideShapeTool } from "@/tools/SlideShapeTool"
|
import { SlideShapeTool } from "@/tools/SlideShapeTool"
|
||||||
import { SlideShapeUtil } from "@/shapes/SlideShapeUtil"
|
import { SlideShape } from "@/shapes/SlideShapeUtil"
|
||||||
import { makeRealSettings, applySettingsMigrations } from "@/lib/settings"
|
import { makeRealSettings, applySettingsMigrations } from "@/lib/settings"
|
||||||
import { PromptShapeTool } from "@/tools/PromptShapeTool"
|
import { PromptShapeTool } from "@/tools/PromptShapeTool"
|
||||||
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
||||||
|
|
@ -38,7 +38,7 @@ const customShapeUtils = [
|
||||||
ChatBoxShape,
|
ChatBoxShape,
|
||||||
VideoChatShape,
|
VideoChatShape,
|
||||||
EmbedShape,
|
EmbedShape,
|
||||||
SlideShapeUtil,
|
SlideShape,
|
||||||
MycrozineTemplateShape,
|
MycrozineTemplateShape,
|
||||||
MarkdownShape,
|
MarkdownShape,
|
||||||
PromptShape,
|
PromptShape,
|
||||||
|
|
|
||||||
|
|
@ -1,165 +1,168 @@
|
||||||
import {
|
import {
|
||||||
BaseBoxShapeUtil,
|
BaseBoxShapeUtil,
|
||||||
HTMLContainer,
|
HTMLContainer,
|
||||||
TLBaseShape,
|
TLBaseShape,
|
||||||
TLGeoShape,
|
TLGeoShape,
|
||||||
TLShape,
|
TLShape,
|
||||||
} from "tldraw"
|
} from "tldraw"
|
||||||
import { getEdge } from "@/propagators/tlgraph"
|
import { getEdge } from "@/propagators/tlgraph"
|
||||||
import { llm } from "@/utils/llm"
|
import { llm } from "@/utils/llm"
|
||||||
import { isShapeOfType } from "@/propagators/utils"
|
import { isShapeOfType } from "@/propagators/utils"
|
||||||
|
|
||||||
type IPrompt = TLBaseShape<
|
type IPrompt = TLBaseShape<
|
||||||
"prompt",
|
"Prompt",
|
||||||
{
|
{
|
||||||
w: number
|
w: number
|
||||||
h: number
|
h: number
|
||||||
prompt: string
|
prompt: string
|
||||||
value: string
|
value: string
|
||||||
agentBinding: string | null
|
agentBinding: string | null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
|
export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
|
||||||
static override type = "prompt" as const
|
static override type = "Prompt" as const
|
||||||
|
|
||||||
FIXED_HEIGHT = 50 as const
|
FIXED_HEIGHT = 50 as const
|
||||||
MIN_WIDTH = 150 as const
|
MIN_WIDTH = 150 as const
|
||||||
PADDING = 4 as const
|
PADDING = 4 as const
|
||||||
|
|
||||||
getDefaultProps(): IPrompt["props"] {
|
getDefaultProps(): IPrompt["props"] {
|
||||||
return {
|
return {
|
||||||
w: 300,
|
w: 300,
|
||||||
h: 50,
|
h: 50,
|
||||||
prompt: "",
|
prompt: "",
|
||||||
value: "",
|
value: "",
|
||||||
agentBinding: null,
|
agentBinding: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// override onResize: TLResizeHandle<IPrompt> = (
|
// override onResize: TLResizeHandle<IPrompt> = (
|
||||||
// shape,
|
// shape,
|
||||||
// { scaleX, initialShape },
|
// { scaleX, initialShape },
|
||||||
// ) => {
|
// ) => {
|
||||||
// const { x, y } = shape
|
// const { x, y } = shape
|
||||||
// const w = initialShape.props.w * scaleX
|
// const w = initialShape.props.w * scaleX
|
||||||
// return {
|
// return {
|
||||||
// x,
|
// x,
|
||||||
// y,
|
// y,
|
||||||
// props: {
|
// props: {
|
||||||
// ...shape.props,
|
// ...shape.props,
|
||||||
// w: Math.max(Math.abs(w), this.MIN_WIDTH),
|
// w: Math.max(Math.abs(w), this.MIN_WIDTH),
|
||||||
// h: this.FIXED_HEIGHT,
|
// h: this.FIXED_HEIGHT,
|
||||||
// },
|
// },
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
component(shape: IPrompt) {
|
component(shape: IPrompt) {
|
||||||
const arrowBindings = this.editor.getBindingsInvolvingShape(
|
const arrowBindings = this.editor.getBindingsInvolvingShape(
|
||||||
shape.id,
|
shape.id,
|
||||||
"arrow",
|
"arrow",
|
||||||
)
|
)
|
||||||
const arrows = arrowBindings
|
const arrows = arrowBindings.map((binding) =>
|
||||||
.map((binding) => this.editor.getShape(binding.fromId))
|
this.editor.getShape(binding.fromId),
|
||||||
|
)
|
||||||
|
|
||||||
const inputMap = arrows.reduce((acc, arrow) => {
|
const inputMap = arrows.reduce((acc, arrow) => {
|
||||||
const edge = getEdge(arrow, this.editor);
|
const edge = getEdge(arrow, this.editor)
|
||||||
if (edge) {
|
if (edge) {
|
||||||
const sourceShape = this.editor.getShape(edge.from);
|
const sourceShape = this.editor.getShape(edge.from)
|
||||||
if (sourceShape && edge.text) {
|
if (sourceShape && edge.text) {
|
||||||
acc[edge.text] = sourceShape;
|
acc[edge.text] = sourceShape
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return acc;
|
return acc
|
||||||
}, {} as Record<string, TLShape>);
|
}, {} as Record<string, TLShape>)
|
||||||
|
|
||||||
const generateText = async (prompt: string) => {
|
const generateText = async (prompt: string) => {
|
||||||
await llm('', prompt, (partial: string, done: boolean) => {
|
await llm("", prompt, (partial: string, done: boolean) => {
|
||||||
console.log("DONE??", done)
|
console.log("DONE??", done)
|
||||||
this.editor.updateShape<IPrompt>({
|
this.editor.updateShape<IPrompt>({
|
||||||
id: shape.id,
|
id: shape.id,
|
||||||
type: "prompt",
|
type: "Prompt",
|
||||||
props: { value: partial, agentBinding: done ? null : 'someone' },
|
props: { value: partial, agentBinding: done ? null : "someone" },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePrompt = () => {
|
const handlePrompt = () => {
|
||||||
if (shape.props.agentBinding) {
|
if (shape.props.agentBinding) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let processedPrompt = shape.props.prompt;
|
let processedPrompt = shape.props.prompt
|
||||||
for (const [key, sourceShape] of Object.entries(inputMap)) {
|
for (const [key, sourceShape] of Object.entries(inputMap)) {
|
||||||
const pattern = `{${key}}`;
|
const pattern = `{${key}}`
|
||||||
if (processedPrompt.includes(pattern)) {
|
if (processedPrompt.includes(pattern)) {
|
||||||
if (isShapeOfType<TLGeoShape>(sourceShape, 'geo')) {
|
if (isShapeOfType<TLGeoShape>(sourceShape, "geo")) {
|
||||||
processedPrompt = processedPrompt.replace(pattern, sourceShape.props.text);
|
processedPrompt = processedPrompt.replace(
|
||||||
}
|
pattern,
|
||||||
}
|
sourceShape.props.text,
|
||||||
}
|
)
|
||||||
console.log(processedPrompt);
|
}
|
||||||
generateText(processedPrompt)
|
}
|
||||||
};
|
}
|
||||||
|
//console.log(processedPrompt)
|
||||||
|
generateText(processedPrompt)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HTMLContainer
|
<HTMLContainer
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
border: "1px solid lightgrey",
|
border: "1px solid lightgrey",
|
||||||
padding: this.PADDING,
|
padding: this.PADDING,
|
||||||
height: this.FIXED_HEIGHT,
|
height: this.FIXED_HEIGHT,
|
||||||
width: shape.props.w,
|
width: shape.props.w,
|
||||||
pointerEvents: "all",
|
pointerEvents: "all",
|
||||||
backgroundColor: "#efefef",
|
backgroundColor: "#efefef",
|
||||||
overflow: "visible",
|
overflow: "visible",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
outline: shape.props.agentBinding ? "2px solid orange" : "none",
|
outline: shape.props.agentBinding ? "2px solid orange" : "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
overflow: "visible",
|
overflow: "visible",
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.05)",
|
backgroundColor: "rgba(0, 0, 0, 0.05)",
|
||||||
border: "1px solid rgba(0, 0, 0, 0.05)",
|
border: "1px solid rgba(0, 0, 0, 0.05)",
|
||||||
borderRadius: 6 - this.PADDING,
|
borderRadius: 6 - this.PADDING,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter prompt..."
|
placeholder="Enter prompt..."
|
||||||
value={shape.props.prompt}
|
value={shape.props.prompt}
|
||||||
onChange={(text) => {
|
onChange={(text) => {
|
||||||
this.editor.updateShape<IPrompt>({
|
this.editor.updateShape<IPrompt>({
|
||||||
id: shape.id,
|
id: shape.id,
|
||||||
type: "prompt",
|
type: "Prompt",
|
||||||
props: { prompt: text.target.value },
|
props: { prompt: text.target.value },
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
style={{
|
style={{
|
||||||
width: 100,
|
width: 100,
|
||||||
height: "100%",
|
height: "100%",
|
||||||
marginLeft: 5,
|
marginLeft: 5,
|
||||||
pointerEvents: "all",
|
pointerEvents: "all",
|
||||||
}}
|
}}
|
||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handlePrompt}
|
onClick={handlePrompt}
|
||||||
>
|
>
|
||||||
Prompt
|
Prompt
|
||||||
</button>
|
</button>
|
||||||
</HTMLContainer>
|
</HTMLContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// [5]
|
indicator(shape: IPrompt) {
|
||||||
indicator(shape: IPrompt) {
|
return <rect width={shape.props.w} height={shape.props.h} rx={5} />
|
||||||
return <rect width={shape.props.w} height={shape.props.h} rx={5} />
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,128 +1,136 @@
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from "react"
|
||||||
import {
|
import {
|
||||||
BaseBoxShapeUtil,
|
BaseBoxShapeUtil,
|
||||||
Geometry2d,
|
Geometry2d,
|
||||||
RecordProps,
|
RecordProps,
|
||||||
Rectangle2d,
|
Rectangle2d,
|
||||||
SVGContainer,
|
SVGContainer,
|
||||||
ShapeUtil,
|
ShapeUtil,
|
||||||
T,
|
T,
|
||||||
TLBaseShape,
|
TLBaseShape,
|
||||||
getPerfectDashProps,
|
getPerfectDashProps,
|
||||||
resizeBox,
|
resizeBox,
|
||||||
useValue,
|
useValue,
|
||||||
} from 'tldraw'
|
} from "tldraw"
|
||||||
import { moveToSlide, useSlides } from '@/slides/useSlides'
|
import { moveToSlide, useSlides } from "@/slides/useSlides"
|
||||||
|
|
||||||
export type ISlideShape = TLBaseShape<
|
export type ISlideShape = TLBaseShape<
|
||||||
'Slide',
|
"Slide",
|
||||||
{
|
{
|
||||||
w: number
|
w: number
|
||||||
h: number
|
h: number
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
export class SlideShapeUtil extends BaseBoxShapeUtil<ISlideShape> {
|
export class SlideShape extends BaseBoxShapeUtil<ISlideShape> {
|
||||||
static override type = "Slide"
|
static override type = "Slide"
|
||||||
|
|
||||||
// static override props = {
|
|
||||||
// w: T.number,
|
|
||||||
// h: T.number,
|
|
||||||
// }
|
|
||||||
|
|
||||||
override canBind = () => false
|
// static override props = {
|
||||||
override hideRotateHandle = () => true
|
// w: T.number,
|
||||||
|
// h: T.number,
|
||||||
|
// }
|
||||||
|
|
||||||
getDefaultProps(): ISlideShape["props"] {
|
override canBind = () => false
|
||||||
return {
|
override hideRotateHandle = () => true
|
||||||
w: 720,
|
|
||||||
h: 480,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getGeometry(shape: ISlideShape): Geometry2d {
|
getDefaultProps(): ISlideShape["props"] {
|
||||||
return new Rectangle2d({
|
return {
|
||||||
width: shape.props.w,
|
w: 720,
|
||||||
height: shape.props.h,
|
h: 480,
|
||||||
isFilled: false,
|
}
|
||||||
})
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override onRotate = (initial: ISlideShape) => initial
|
getGeometry(shape: ISlideShape): Geometry2d {
|
||||||
override onResize(shape: ISlideShape, info: any) {
|
return new Rectangle2d({
|
||||||
return resizeBox(shape, info)
|
width: shape.props.w,
|
||||||
}
|
height: shape.props.h,
|
||||||
|
isFilled: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
override onDoubleClick = (shape: ISlideShape) => {
|
override onRotate = (initial: ISlideShape) => initial
|
||||||
moveToSlide(this.editor, shape)
|
override onResize(shape: ISlideShape, info: any) {
|
||||||
this.editor.selectNone()
|
return resizeBox(shape, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onDoubleClickEdge = (shape: ISlideShape) => {
|
override onDoubleClick = (shape: ISlideShape) => {
|
||||||
moveToSlide(this.editor, shape)
|
moveToSlide(this.editor, shape)
|
||||||
this.editor.selectNone()
|
this.editor.selectNone()
|
||||||
}
|
}
|
||||||
|
|
||||||
component(shape: ISlideShape) {
|
override onDoubleClickEdge = (shape: ISlideShape) => {
|
||||||
const bounds = this.editor.getShapeGeometry(shape).bounds
|
moveToSlide(this.editor, shape)
|
||||||
|
this.editor.selectNone()
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
component(shape: ISlideShape) {
|
||||||
const zoomLevel = useValue('zoom level', () => this.editor.getZoomLevel(), [this.editor])
|
const bounds = this.editor.getShapeGeometry(shape).bounds
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
const slides = useSlides()
|
const zoomLevel = useValue("zoom level", () => this.editor.getZoomLevel(), [
|
||||||
const index = slides.findIndex((s) => s.id === shape.id)
|
this.editor,
|
||||||
|
])
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
const handleLabelPointerDown = useCallback(() => this.editor.select(shape.id), [shape.id])
|
const slides = useSlides()
|
||||||
|
const index = slides.findIndex((s) => s.id === shape.id)
|
||||||
|
|
||||||
if (!bounds) return null
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const handleLabelPointerDown = useCallback(
|
||||||
|
() => this.editor.select(shape.id),
|
||||||
|
[shape.id],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
if (!bounds) return null
|
||||||
<>
|
|
||||||
<div onPointerDown={handleLabelPointerDown} className="slide-shape-label">
|
|
||||||
{`Slide ${index + 1}`}
|
|
||||||
</div>
|
|
||||||
<SVGContainer>
|
|
||||||
<g
|
|
||||||
style={{
|
|
||||||
stroke: 'var(--color-text)',
|
|
||||||
strokeWidth: 'calc(1px * var(--tl-scale))',
|
|
||||||
opacity: 0.25,
|
|
||||||
}}
|
|
||||||
pointerEvents="none"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
{bounds.sides.map((side, i) => {
|
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
|
||||||
side[0].dist(side[1]),
|
|
||||||
1 / zoomLevel,
|
|
||||||
{
|
|
||||||
style: 'dashed',
|
|
||||||
lengthRatio: 6,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<line
|
<>
|
||||||
key={i}
|
<div
|
||||||
x1={side[0].x}
|
onPointerDown={handleLabelPointerDown}
|
||||||
y1={side[0].y}
|
className="slide-shape-label"
|
||||||
x2={side[1].x}
|
>
|
||||||
y2={side[1].y}
|
{`Slide ${index + 1}`}
|
||||||
strokeDasharray={strokeDasharray}
|
</div>
|
||||||
strokeDashoffset={strokeDashoffset}
|
<SVGContainer>
|
||||||
/>
|
<g
|
||||||
)
|
style={{
|
||||||
})}
|
stroke: "var(--color-text)",
|
||||||
</g>
|
strokeWidth: "calc(1px * var(--tl-scale))",
|
||||||
</SVGContainer>
|
opacity: 0.25,
|
||||||
</>
|
}}
|
||||||
)
|
pointerEvents="none"
|
||||||
}
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
{bounds.sides.map((side, i) => {
|
||||||
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
|
side[0].dist(side[1]),
|
||||||
|
1 / zoomLevel,
|
||||||
|
{
|
||||||
|
style: "dashed",
|
||||||
|
lengthRatio: 6,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
indicator(shape: ISlideShape) {
|
return (
|
||||||
return <rect width={shape.props.w} height={shape.props.h} />
|
<line
|
||||||
}
|
key={i}
|
||||||
}
|
x1={side[0].x}
|
||||||
|
y1={side[0].y}
|
||||||
|
x2={side[1].x}
|
||||||
|
y2={side[1].y}
|
||||||
|
strokeDasharray={strokeDasharray}
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</g>
|
||||||
|
</SVGContainer>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator(shape: ISlideShape) {
|
||||||
|
return <rect width={shape.props.w} height={shape.props.h} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { BaseBoxShapeTool } from 'tldraw'
|
import { BaseBoxShapeTool } from 'tldraw'
|
||||||
|
|
||||||
export class PromptShapeTool extends BaseBoxShapeTool {
|
export class PromptShapeTool extends BaseBoxShapeTool {
|
||||||
static override id = 'prompt'
|
static override id = 'Prompt'
|
||||||
static override initial = 'idle'
|
static override initial = 'idle'
|
||||||
override shapeType = 'prompt'
|
override shapeType = 'Prompt'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -7,6 +7,6 @@ export class SlideShapeTool extends BaseBoxShapeTool {
|
||||||
|
|
||||||
constructor(editor: any) {
|
constructor(editor: any) {
|
||||||
super(editor)
|
super(editor)
|
||||||
console.log('SlideShapeTool constructed', { id: this.id, shapeType: this.shapeType })
|
//console.log('SlideShapeTool constructed', { id: this.id, shapeType: this.shapeType })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -142,7 +142,7 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TldrawUiMenuItem
|
<TldrawUiMenuItem
|
||||||
id="mycrozine-template"
|
id="MycrozineTemplate"
|
||||||
label="Create Mycrozine Template"
|
label="Create Mycrozine Template"
|
||||||
icon="rectangle"
|
icon="rectangle"
|
||||||
kbd="m"
|
kbd="m"
|
||||||
|
|
@ -152,7 +152,7 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TldrawUiMenuItem
|
<TldrawUiMenuItem
|
||||||
id="markdown"
|
id="Markdown"
|
||||||
label="Create Markdown"
|
label="Create Markdown"
|
||||||
icon="markdown"
|
icon="markdown"
|
||||||
kbd="alt+m"
|
kbd="alt+m"
|
||||||
|
|
@ -161,6 +161,16 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
|
||||||
editor.setCurrentTool("Markdown")
|
editor.setCurrentTool("Markdown")
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<TldrawUiMenuItem
|
||||||
|
id="Prompt"
|
||||||
|
label="Create Prompt"
|
||||||
|
icon="prompt"
|
||||||
|
kbd="alt+p"
|
||||||
|
disabled={hasSelection}
|
||||||
|
onSelect={() => {
|
||||||
|
editor.setCurrentTool("Prompt")
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</TldrawUiMenuGroup>
|
</TldrawUiMenuGroup>
|
||||||
|
|
||||||
{/* Frame Controls */}
|
{/* Frame Controls */}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,15 @@ import { DefaultToolbar, DefaultToolbarContent } from "tldraw"
|
||||||
import { useTools } from "tldraw"
|
import { useTools } from "tldraw"
|
||||||
import { useEditor } from "tldraw"
|
import { useEditor } from "tldraw"
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
|
import { useDialogs } from "tldraw"
|
||||||
|
import { SettingsDialog } from "./SettingsDialog"
|
||||||
|
|
||||||
export function CustomToolbar() {
|
export function CustomToolbar() {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
const tools = useTools()
|
const tools = useTools()
|
||||||
const [isReady, setIsReady] = useState(false)
|
const [isReady, setIsReady] = useState(false)
|
||||||
|
const [hasApiKey, setHasApiKey] = useState(false)
|
||||||
|
const { addDialog, removeDialog } = useDialogs()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editor && tools) {
|
if (editor && tools) {
|
||||||
|
|
@ -15,53 +19,132 @@ export function CustomToolbar() {
|
||||||
}
|
}
|
||||||
}, [editor, tools])
|
}, [editor, tools])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const settings = localStorage.getItem("jeff_keys")
|
||||||
|
if (settings) {
|
||||||
|
const { keys } = JSON.parse(settings)
|
||||||
|
setHasApiKey(Object.values(keys).some((key) => key))
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
if (!isReady) return null
|
if (!isReady) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DefaultToolbar>
|
<div style={{ position: "relative" }}>
|
||||||
<DefaultToolbarContent />
|
<div
|
||||||
{tools["VideoChat"] && (
|
style={{
|
||||||
<TldrawUiMenuItem
|
position: "fixed",
|
||||||
{...tools["VideoChat"]}
|
top: "40px",
|
||||||
icon="video"
|
right: "12px",
|
||||||
label="Video Chat"
|
zIndex: 99999,
|
||||||
isSelected={tools["VideoChat"].id === editor.getCurrentToolId()}
|
pointerEvents: "auto",
|
||||||
/>
|
display: "flex",
|
||||||
)}
|
gap: "8px",
|
||||||
{tools["ChatBox"] && (
|
}}
|
||||||
<TldrawUiMenuItem
|
>
|
||||||
{...tools["ChatBox"]}
|
<button
|
||||||
icon="chat"
|
onClick={() => {
|
||||||
label="Chat"
|
addDialog({
|
||||||
isSelected={tools["ChatBox"].id === editor.getCurrentToolId()}
|
id: "api-keys",
|
||||||
/>
|
component: ({ onClose }: { onClose: () => void }) => (
|
||||||
)}
|
<SettingsDialog
|
||||||
{tools["Embed"] && (
|
onClose={() => {
|
||||||
<TldrawUiMenuItem
|
onClose()
|
||||||
{...tools["Embed"]}
|
removeDialog("api-keys")
|
||||||
icon="embed"
|
const settings = localStorage.getItem("jeff_keys")
|
||||||
label="Embed"
|
if (settings) {
|
||||||
isSelected={tools["Embed"].id === editor.getCurrentToolId()}
|
const { keys } = JSON.parse(settings)
|
||||||
/>
|
setHasApiKey(Object.values(keys).some((key) => key))
|
||||||
)}
|
}
|
||||||
{tools["SlideShape"] && (
|
}}
|
||||||
<TldrawUiMenuItem
|
/>
|
||||||
{...tools["SlideShape"]}
|
),
|
||||||
icon="slides"
|
})
|
||||||
label="Slide"
|
}}
|
||||||
isSelected={tools["SlideShape"].id === editor.getCurrentToolId()}
|
style={{
|
||||||
/>
|
padding: "8px 16px",
|
||||||
)}
|
borderRadius: "4px",
|
||||||
{/*
|
background: "#2F80ED",
|
||||||
{tools["Markdown"] && (
|
color: "white",
|
||||||
<TldrawUiMenuItem
|
border: "none",
|
||||||
{...tools["Markdown"]}
|
cursor: "pointer",
|
||||||
icon="markdown"
|
fontWeight: 500,
|
||||||
label="Markdown"
|
transition: "background 0.2s ease",
|
||||||
isSelected={tools["Markdown"].id === editor.getCurrentToolId()}
|
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
|
||||||
/>
|
whiteSpace: "nowrap",
|
||||||
)}
|
userSelect: "none",
|
||||||
*/}
|
}}
|
||||||
</DefaultToolbar>
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = "#1366D6"
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = "#2F80ED"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Keys {hasApiKey ? "✅" : "❌"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<DefaultToolbar>
|
||||||
|
<DefaultToolbarContent />
|
||||||
|
{tools["VideoChat"] && (
|
||||||
|
<TldrawUiMenuItem
|
||||||
|
{...tools["VideoChat"]}
|
||||||
|
icon="video"
|
||||||
|
label="Video Chat"
|
||||||
|
isSelected={tools["VideoChat"].id === editor.getCurrentToolId()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tools["ChatBox"] && (
|
||||||
|
<TldrawUiMenuItem
|
||||||
|
{...tools["ChatBox"]}
|
||||||
|
icon="chat"
|
||||||
|
label="Chat"
|
||||||
|
isSelected={tools["ChatBox"].id === editor.getCurrentToolId()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tools["Embed"] && (
|
||||||
|
<TldrawUiMenuItem
|
||||||
|
{...tools["Embed"]}
|
||||||
|
icon="embed"
|
||||||
|
label="Embed"
|
||||||
|
isSelected={tools["Embed"].id === editor.getCurrentToolId()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tools["SlideShape"] && (
|
||||||
|
<TldrawUiMenuItem
|
||||||
|
{...tools["SlideShape"]}
|
||||||
|
icon="slides"
|
||||||
|
label="Slide"
|
||||||
|
isSelected={tools["SlideShape"].id === editor.getCurrentToolId()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tools["Markdown"] && (
|
||||||
|
<TldrawUiMenuItem
|
||||||
|
{...tools["Markdown"]}
|
||||||
|
icon="markdown"
|
||||||
|
label="Markdown"
|
||||||
|
isSelected={tools["Markdown"].id === editor.getCurrentToolId()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tools["MycrozineTemplate"] && (
|
||||||
|
<TldrawUiMenuItem
|
||||||
|
{...tools["MycrozineTemplate"]}
|
||||||
|
icon="mycrozinetemplate"
|
||||||
|
label="MycrozineTemplate"
|
||||||
|
isSelected={
|
||||||
|
tools["MycrozineTemplate"].id === editor.getCurrentToolId()
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tools["Prompt"] && (
|
||||||
|
<TldrawUiMenuItem
|
||||||
|
{...tools["Prompt"]}
|
||||||
|
icon="prompt"
|
||||||
|
label="Prompt"
|
||||||
|
isSelected={tools["Prompt"].id === editor.getCurrentToolId()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DefaultToolbar>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,102 +4,17 @@ import { CustomContextMenu } from "./CustomContextMenu"
|
||||||
import {
|
import {
|
||||||
DefaultKeyboardShortcutsDialog,
|
DefaultKeyboardShortcutsDialog,
|
||||||
DefaultKeyboardShortcutsDialogContent,
|
DefaultKeyboardShortcutsDialogContent,
|
||||||
DefaultToolbar,
|
|
||||||
DefaultToolbarContent,
|
|
||||||
TLComponents,
|
TLComponents,
|
||||||
TldrawUiMenuItem,
|
TldrawUiMenuItem,
|
||||||
useDialogs,
|
|
||||||
useIsToolSelected,
|
|
||||||
useTools,
|
useTools,
|
||||||
} from "tldraw"
|
} from "tldraw"
|
||||||
import { SettingsDialog } from "./SettingsDialog"
|
|
||||||
import { useEffect } from "react"
|
|
||||||
import { SlidesPanel } from "@/slides/SlidesPanel"
|
import { SlidesPanel } from "@/slides/SlidesPanel"
|
||||||
import { useState } from "react"
|
|
||||||
|
|
||||||
export const components: TLComponents = {
|
export const components: TLComponents = {
|
||||||
// Toolbar: CustomToolbar,
|
Toolbar: CustomToolbar,
|
||||||
MainMenu: CustomMainMenu,
|
MainMenu: CustomMainMenu,
|
||||||
ContextMenu: CustomContextMenu,
|
ContextMenu: CustomContextMenu,
|
||||||
HelperButtons: SlidesPanel,
|
HelperButtons: SlidesPanel,
|
||||||
Toolbar: (props: any) => {
|
|
||||||
const tools = useTools()
|
|
||||||
const slideTool = tools["Slide"]
|
|
||||||
const isSlideSelected = slideTool ? useIsToolSelected(slideTool) : false
|
|
||||||
const { addDialog, removeDialog } = useDialogs()
|
|
||||||
const [hasApiKey, setHasApiKey] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const key = localStorage.getItem("openai_api_key")
|
|
||||||
setHasApiKey(!!key)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ position: "relative", width: "100%", height: "100%" }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
top: "40px",
|
|
||||||
right: "12px",
|
|
||||||
zIndex: 99999,
|
|
||||||
pointerEvents: "auto",
|
|
||||||
display: "flex",
|
|
||||||
gap: "8px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
(
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
addDialog({
|
|
||||||
id: "api-keys",
|
|
||||||
component: ({ onClose }: { onClose: () => void }) => (
|
|
||||||
<SettingsDialog
|
|
||||||
onClose={() => {
|
|
||||||
onClose()
|
|
||||||
removeDialog("api-keys")
|
|
||||||
const settings = localStorage.getItem("jeff_keys")
|
|
||||||
if (settings) {
|
|
||||||
const { keys } = JSON.parse(settings)
|
|
||||||
setHasApiKey(Object.values(keys).some((key) => key))
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
padding: "8px 16px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
background: "#2F80ED",
|
|
||||||
color: "white",
|
|
||||||
border: "none",
|
|
||||||
cursor: "pointer",
|
|
||||||
fontWeight: 500,
|
|
||||||
transition: "background 0.2s ease",
|
|
||||||
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
userSelect: "none",
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.background = "#1366D6"
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.background = "#2F80ED"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Keys {hasApiKey ? "✅" : "❌"}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
</div>
|
|
||||||
<DefaultToolbar {...props}>
|
|
||||||
{slideTool && (
|
|
||||||
<TldrawUiMenuItem {...slideTool} isSelected={isSlideSelected} />
|
|
||||||
)}
|
|
||||||
<DefaultToolbarContent />
|
|
||||||
</DefaultToolbar>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
KeyboardShortcutsDialog: (props: any) => {
|
KeyboardShortcutsDialog: (props: any) => {
|
||||||
const tools = useTools()
|
const tools = useTools()
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -106,10 +106,7 @@ export const overrides: TLUiOverrides = {
|
||||||
type: "Slide",
|
type: "Slide",
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
console.log("SlideShape tool selected from menu")
|
|
||||||
console.log("Current tool before:", editor.getCurrentToolId())
|
|
||||||
editor.setCurrentTool("Slide")
|
editor.setCurrentTool("Slide")
|
||||||
console.log("Current tool after:", editor.getCurrentToolId())
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Markdown: {
|
Markdown: {
|
||||||
|
|
@ -130,6 +127,15 @@ export const overrides: TLUiOverrides = {
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect: () => editor.setCurrentTool("MycrozineTemplate"),
|
onSelect: () => editor.setCurrentTool("MycrozineTemplate"),
|
||||||
},
|
},
|
||||||
|
Prompt: {
|
||||||
|
id: "Prompt",
|
||||||
|
icon: "prompt",
|
||||||
|
label: "Prompt",
|
||||||
|
type: "Prompt",
|
||||||
|
kdb: "p",
|
||||||
|
readonlyOk: true,
|
||||||
|
onSelect: () => editor.setCurrentTool("Prompt"),
|
||||||
|
},
|
||||||
hand: {
|
hand: {
|
||||||
...tools.hand,
|
...tools.hand,
|
||||||
onDoubleClick: (info: any) => {
|
onDoubleClick: (info: any) => {
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,7 @@ import { VideoChatShape } from "@/shapes/VideoChatShapeUtil"
|
||||||
import { EmbedShape } from "@/shapes/EmbedShapeUtil"
|
import { EmbedShape } from "@/shapes/EmbedShapeUtil"
|
||||||
import { MarkdownShape } from "@/shapes/MarkdownShapeUtil"
|
import { MarkdownShape } from "@/shapes/MarkdownShapeUtil"
|
||||||
import { MycrozineTemplateShape } from "@/shapes/MycrozineTemplateShapeUtil"
|
import { MycrozineTemplateShape } from "@/shapes/MycrozineTemplateShapeUtil"
|
||||||
import { T } from "@tldraw/tldraw"
|
import { SlideShape } from "@/shapes/SlideShapeUtil"
|
||||||
import { SlideShapeUtil } from "@/shapes/SlideShapeUtil"
|
|
||||||
|
|
||||||
// add custom shapes and bindings here if needed:
|
// add custom shapes and bindings here if needed:
|
||||||
export const customSchema = createTLSchema({
|
export const customSchema = createTLSchema({
|
||||||
|
|
@ -45,8 +44,8 @@ export const customSchema = createTLSchema({
|
||||||
migrations: MycrozineTemplateShape.migrations,
|
migrations: MycrozineTemplateShape.migrations,
|
||||||
},
|
},
|
||||||
Slide: {
|
Slide: {
|
||||||
props: SlideShapeUtil.props,
|
props: SlideShape.props,
|
||||||
migrations: SlideShapeUtil.migrations,
|
migrations: SlideShape.migrations,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bindings: defaultBindingSchemas,
|
bindings: defaultBindingSchemas,
|
||||||
|
|
@ -225,6 +224,7 @@ export class TldrawDurableObject {
|
||||||
onDataChange: () => {
|
onDataChange: () => {
|
||||||
// and persist whenever the data in the room changes
|
// and persist whenever the data in the room changes
|
||||||
this.schedulePersistToR2()
|
this.schedulePersistToR2()
|
||||||
|
console.log("Persisting", this.roomId, "to R2")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})()
|
})()
|
||||||
|
|
@ -237,7 +237,7 @@ export class TldrawDurableObject {
|
||||||
schedulePersistToR2 = throttle(async () => {
|
schedulePersistToR2 = throttle(async () => {
|
||||||
if (!this.roomPromise || !this.roomId) return
|
if (!this.roomPromise || !this.roomId) return
|
||||||
const room = await this.getRoom()
|
const room = await this.getRoom()
|
||||||
|
|
||||||
// convert the room to JSON and upload it to R2
|
// convert the room to JSON and upload it to R2
|
||||||
const snapshot = JSON.stringify(room.getCurrentSnapshot())
|
const snapshot = JSON.stringify(room.getCurrentSnapshot())
|
||||||
await this.r2.put(`rooms/${this.roomId}`, snapshot)
|
await this.r2.put(`rooms/${this.roomId}`, snapshot)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue