diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..470aea0 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "trailingComma": "es5", + "singleQuote": true, + "semi": false, + "printWidth": 100, + "tabWidth": 2, + "useTabs": true, + "plugins": [ + "prettier-plugin-organize-imports" + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 47788b0..b0a90a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,8 +7,9 @@ "": { "name": "tldraw-fal", "version": "0.1.0", + "license": "MIT", "dependencies": { - "@fal-ai/serverless-client": "^0.5.4", + "@fal-ai/serverless-client": "^0.6.0-alpha.4", "@fal-ai/serverless-proxy": "^0.5.0", "@tldraw/tldraw": "^2.0.0-canary.ba4091c59418", "next": "14.0.3", @@ -24,6 +25,7 @@ "eslint-config-next": "14.0.3", "postcss": "^8", "prettier": "^3.1.0", + "prettier-plugin-organize-imports": "^3.2.4", "tailwindcss": "^3.3.0", "typescript": "^5" } @@ -117,9 +119,9 @@ } }, "node_modules/@fal-ai/serverless-client": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@fal-ai/serverless-client/-/serverless-client-0.5.4.tgz", - "integrity": "sha512-8eTA+lBXtGzYqDTjIHm7vnViu5AlPZYR1D9BQbsDxro/K53W9sMhZtc7QSboE1MMcbs4hW2B+f+Jkkx39hJCPw==" + "version": "0.6.0-alpha.4", + "resolved": "https://registry.npmjs.org/@fal-ai/serverless-client/-/serverless-client-0.6.0-alpha.4.tgz", + "integrity": "sha512-T9fqiMU1LohzzqsNZjY4zq8ZMxJcZo8eYvSXaz2i6nmb9sQxuQ4hCB/YTeZJ2Ong4P+ZOMkHPMD2kW4pNgf5gw==" }, "node_modules/@fal-ai/serverless-proxy": { "version": "0.5.0", @@ -4642,6 +4644,26 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-plugin-organize-imports": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz", + "integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==", + "dev": true, + "peerDependencies": { + "@volar/vue-language-plugin-pug": "^1.0.4", + "@volar/vue-typescript": "^1.0.4", + "prettier": ">=2.0", + "typescript": ">=2.9" + }, + "peerDependenciesMeta": { + "@volar/vue-language-plugin-pug": { + "optional": true + }, + "@volar/vue-typescript": { + "optional": true + } + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/package.json b/package.json index 828948f..ef3de68 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "tldraw-fal", "version": "0.1.0", "private": true, + "license": "MIT", "scripts": { "dev": "next dev", "build": "next build", @@ -10,7 +11,7 @@ "format": "prettier --write ." }, "dependencies": { - "@fal-ai/serverless-client": "^0.5.4", + "@fal-ai/serverless-client": "^0.6.0-alpha.4", "@fal-ai/serverless-proxy": "^0.5.0", "@tldraw/tldraw": "^2.0.0-canary.ba4091c59418", "next": "14.0.3", @@ -26,7 +27,8 @@ "eslint-config-next": "14.0.3", "postcss": "^8", "prettier": "^3.1.0", + "prettier-plugin-organize-imports": "^3.2.4", "tailwindcss": "^3.3.0", "typescript": "^5" } -} +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index 3d01545..ef11bb2 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,18 +1,9 @@ 'use client' -import { - LiveImageShape, - LiveImageShapeUtil, -} from '@/components/LiveImageShapeUtil' +import { LiveImageShape, LiveImageShapeUtil } from '@/components/LiveImageShapeUtil' import * as fal from '@fal-ai/serverless-client' -import { - AssetRecordType, - Editor, - FrameShapeTool, - Tldraw, - useEditor, -} from '@tldraw/tldraw' -import { useCallback, useEffect } from 'react' +import { Editor, Tldraw, useEditor } from '@tldraw/tldraw' +import { useEffect } from 'react' import { LiveImageTool, MakeLiveButton } from '../components/LiveImageTool' fal.config({ diff --git a/src/components/FrameHeading.tsx b/src/components/FrameHeading.tsx index 760a8d9..43a0e00 100644 --- a/src/components/FrameHeading.tsx +++ b/src/components/FrameHeading.tsx @@ -8,9 +8,9 @@ import { useIsEditing, useValue, } from '@tldraw/editor' +import { preventDefault, stopEventPropagation } from '@tldraw/tldraw' import { useCallback, useEffect, useRef } from 'react' import { FrameLabelInput } from './FrameLabelInput' -import { preventDefault, stopEventPropagation } from '@tldraw/tldraw' export function FrameHeading({ id, @@ -41,8 +41,6 @@ export function FrameHeading({ const event = getPointerInfo(e) - console.log('hello') - // If we're editing the frame label, we shouldn't hijack the pointer event if (editor.getEditingShapeId() === id) return @@ -77,9 +75,9 @@ export function FrameHeading({ // rotate right 45 deg const offsetRotation = pageRotation + Math.PI / 4 const scaledRotation = (offsetRotation * (2 / Math.PI) + 4) % 4 - const labelSide: SelectionEdge = ( - ['top', 'left', 'bottom', 'right'] as const - )[Math.floor(scaledRotation)] + const labelSide: SelectionEdge = (['top', 'left', 'bottom', 'right'] as const)[ + Math.floor(scaledRotation) + ] let labelTranslate: string switch (labelSide) { @@ -87,9 +85,7 @@ export function FrameHeading({ labelTranslate = `` break case 'right': - labelTranslate = `translate(${toDomPrecision( - width - )}px, 0px) rotate(90deg)` + labelTranslate = `translate(${toDomPrecision(width)}px, 0px) rotate(90deg)` break case 'bottom': labelTranslate = `translate(${toDomPrecision(width)}px, ${toDomPrecision( @@ -97,9 +93,7 @@ export function FrameHeading({ )}px) rotate(180deg)` break case 'left': - labelTranslate = `translate(0px, ${toDomPrecision( - height - )}px) rotate(270deg)` + labelTranslate = `translate(0px, ${toDomPrecision(height)}px) rotate(270deg)` break } @@ -109,9 +103,7 @@ export function FrameHeading({ style={{ overflow: isEditing ? 'visible' : 'hidden', maxWidth: `calc(var(--tl-zoom) * ${ - labelSide === 'top' || labelSide === 'bottom' - ? Math.ceil(width) - : Math.ceil(height) + labelSide === 'top' || labelSide === 'bottom' ? Math.ceil(width) : Math.ceil(height) }px + var(--space-5))`, bottom: '100%', transform: `${labelTranslate} scale(var(--tl-scale)) translateX(calc(-1 * var(--space-3))`, @@ -119,12 +111,7 @@ export function FrameHeading({ onPointerDown={handlePointerDown} >
- +
) diff --git a/src/components/LiveImageShapeUtil.tsx b/src/components/LiveImageShapeUtil.tsx index 7e8a7c9..3662e2b 100644 --- a/src/components/LiveImageShapeUtil.tsx +++ b/src/components/LiveImageShapeUtil.tsx @@ -2,20 +2,12 @@ /* eslint-disable react-hooks/rules-of-hooks */ import { AssetRecordType, - canonicalizeRotation, - FrameShapeUtil, Geometry2d, getDefaultColorTheme, - getHashForObject, - getSvgAsImage, - HTMLContainer, - IdOf, Rectangle2d, resizeBox, - SelectionEdge, ShapeUtil, SVGContainer, - TLAsset, TLBaseShape, TLGroupShape, TLOnResizeEndHandler, @@ -27,15 +19,8 @@ import { useIsDarkMode, } from '@tldraw/tldraw' -import { blobToDataUri } from '@/utils/blob' -import { debounce } from '@/utils/debounce' -import * as fal from '@fal-ai/serverless-client' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import result from 'postcss/lib/result' -import { FrameHeading } from './FrameHeading' -import image from 'next/image' -import { connect } from 'http2' import { useFal } from '@/hooks/useFal' +import { FrameHeading } from './FrameHeading' // See https://www.fal.ai/models/latent-consistency-sd @@ -94,10 +79,7 @@ export class LiveImageShapeUtil extends ShapeUtil { canUnmount = () => false - override canReceiveNewChildrenOfType = ( - shape: TLShape, - _type: TLShape['type'] - ) => { + override canReceiveNewChildrenOfType = (shape: TLShape, _type: TLShape['type']) => { return !shape.isLocked } @@ -105,10 +87,7 @@ export class LiveImageShapeUtil extends ShapeUtil { return true } - override canDropShapes = ( - shape: LiveImageShape, - _shapes: TLShape[] - ): boolean => { + override canDropShapes = (shape: LiveImageShape, _shapes: TLShape[]): boolean => { return !shape.isLocked } @@ -126,13 +105,9 @@ export class LiveImageShapeUtil extends ShapeUtil { return { shouldHint: false } } - override onDragShapesOut = ( - _shape: LiveImageShape, - shapes: TLShape[] - ): void => { + override onDragShapesOut = (_shape: LiveImageShape, shapes: TLShape[]): void => { const parent = this.editor.getShape(_shape.parentId) - const isInGroup = - parent && this.editor.isShapeOfType(parent, 'group') + const isInGroup = parent && this.editor.isShapeOfType(parent, 'group') // If frame is in a group, keep the shape // moved out in that group @@ -158,10 +133,7 @@ export class LiveImageShapeUtil extends ShapeUtil { } if (shapesToReparent.length > 0) { - this.editor.reparentShapes( - shapesToReparent, - this.editor.getCurrentPageId() - ) + this.editor.reparentShapes(shapesToReparent, this.editor.getCurrentPageId()) } } @@ -186,7 +158,7 @@ export class LiveImageShapeUtil extends ShapeUtil { useFal(shape.id, { debounceTime: 0, - url: 'wss://110602490-lcm-sd15-i2i.gateway.alpha.fal.ai/ws', + appId: '110602490-lcm-sd15-i2i', }) const bounds = this.editor.getShapeGeometry(shape).bounds diff --git a/src/hooks/useFal.ts b/src/hooks/useFal.ts index 739dabb..a35f19d 100644 --- a/src/hooks/useFal.ts +++ b/src/hooks/useFal.ts @@ -1,5 +1,6 @@ import { LiveImageShape } from '@/components/LiveImageShapeUtil' import { blobToDataUri } from '@/utils/blob' +import * as fal from '@fal-ai/serverless-client' import { AssetRecordType, TLShape, @@ -10,17 +11,17 @@ import { throttle, useEditor, } from '@tldraw/tldraw' -import { useRef, useEffect } from 'react' +import { useEffect, useRef } from 'react' export function useFal( shapeId: TLShapeId, opts: { debounceTime?: number throttleTime?: number - url: string + appId: string } ) { - const { url, throttleTime = 500, debounceTime = 0 } = opts + const { appId, throttleTime = 500, debounceTime = 0 } = opts const editor = useEditor() const startedIteration = useRef(0) const finishedIteration = useRef(0) @@ -28,10 +29,6 @@ export function useFal( const prevHash = useRef(null) useEffect(() => { - let socket: WebSocket | null = null - - let isReconnecting = false - function updateImage(url: string | null) { const shape = editor.getShape(shapeId)! const id = AssetRecordType.createId(shape.id.split(':')[1]) @@ -69,60 +66,26 @@ export function useFal( } } - async function connect() { - { - socket = new WebSocket(url) - socket.onopen = () => { - // console.log("WebSocket Open"); + const { send: sendCurrentData, close } = fal.realtime.connect(appId, { + connectionKey: 'fal-realtime-example', + clientOnly: false, + throttleInterval: 1000, + onError: (error) => { + console.error(error) + }, + onResult: (result) => { + console.log(result) + if (result.images && result.images[0]) { + updateImage(result.images[0].url) } - - socket.onclose = () => { - // console.log("WebSocket Close"); - } - - socket.onerror = (error) => { - // console.error("WebSocket Error:", error); - } - - socket.onmessage = (message) => { - try { - const data = JSON.parse(message.data) - // console.log("WebSocket Message:", data); - if (data.images && data.images.length > 0) { - updateImage(data.images[0].url ?? '') - } - } catch (e) { - console.error('Error parsing the WebSocket response:', e) - } - } - } - } - - async function sendCurrentData(message: string) { - if (!isReconnecting && socket?.readyState !== WebSocket.OPEN) { - isReconnecting = true - connect() - } - - if (isReconnecting && socket?.readyState !== WebSocket.OPEN) { - await new Promise((resolve) => { - const checkConnection = setInterval(() => { - if (socket?.readyState === WebSocket.OPEN) { - clearInterval(checkConnection) - resolve() - } - }, 100) - }) - isReconnecting = false - } - socket?.send(message) - } + }, + }) async function updateDrawing() { const iteration = startedIteration.current++ - const shapes = Array.from(editor.getShapeAndDescendantIds([shapeId])).map( - (id) => editor.getShape(id) + const shapes = Array.from(editor.getShapeAndDescendantIds([shapeId])).map((id) => + editor.getShape(id) ) as TLShape[] const hash = getHashForObject(shapes) @@ -160,18 +123,20 @@ export function useFal( ? shape.props.name + ' hd award-winning impressive' : 'A random image that is safe for work and not surprising—something boring like a city or shoe watercolor' const imageDataUri = await blobToDataUri(image) - const request = { - image_url: imageDataUri, - prompt, - sync_mode: true, - strength: 0.7, - seed: 42, // TODO make this configurable in the UI - enable_safety_checks: false, - } - // We might be stale if (iteration <= finishedIteration.current) return - sendCurrentData(JSON.stringify(request)) + + console.log('ok here we go') + sendCurrentData( + JSON.stringify({ + prompt, + image_url: imageDataUri, + sync_mode: true, + strength: 0.7, + seed: 11252023, // TODO make this configurable in the UI + enable_safety_checks: false, + }) + ) finishedIteration.current = iteration } @@ -184,7 +149,8 @@ export function useFal( editor.on('update-drawings' as any, onDrawingChange) return () => { + close() editor.off('update-drawings' as any, onDrawingChange) } - }, [editor, shapeId, throttleTime, debounceTime, url]) + }, [editor, shapeId, throttleTime, debounceTime, appId]) }