diff --git a/web/app/components/workflow-app/components/workflow-main.tsx b/web/app/components/workflow-app/components/workflow-main.tsx index 6bc2fbcc31..067e9ccf93 100644 --- a/web/app/components/workflow-app/components/workflow-main.tsx +++ b/web/app/components/workflow-app/components/workflow-main.tsx @@ -27,7 +27,7 @@ import { useStore, useWorkflowStore } from '@/app/components/workflow/store' import { useCollaboration } from '@/app/components/workflow/collaboration' import { collaborationManager } from '@/app/components/workflow/collaboration' import { fetchWorkflowDraft } from '@/service/workflow' -import { useStoreApi } from 'reactflow' +import { useReactFlow, useStoreApi } from 'reactflow' type WorkflowMainProps = Pick const WorkflowMain = ({ @@ -39,6 +39,7 @@ const WorkflowMain = ({ const workflowStore = useWorkflowStore() const appId = useStore(s => s.appId) const containerRef = useRef(null) + const reactFlow = useReactFlow() const store = useStoreApi() const { startCursorTracking, stopCursorTracking, onlineUsers, cursors, isConnected } = useCollaboration(appId, store) @@ -55,12 +56,12 @@ const WorkflowMain = ({ useEffect(() => { if (containerRef.current) - startCursorTracking(containerRef as React.RefObject) + startCursorTracking(containerRef as React.RefObject, reactFlow) return () => { stopCursorTracking() } - }, [startCursorTracking, stopCursorTracking]) + }, [startCursorTracking, stopCursorTracking, reactFlow]) const handleWorkflowDataUpdate = useCallback((payload: any) => { const { diff --git a/web/app/components/workflow/collaboration/components/user-cursors.tsx b/web/app/components/workflow/collaboration/components/user-cursors.tsx index e5ec93cf6b..bcc94960e3 100644 --- a/web/app/components/workflow/collaboration/components/user-cursors.tsx +++ b/web/app/components/workflow/collaboration/components/user-cursors.tsx @@ -1,4 +1,5 @@ import type { FC } from 'react' +import { useViewport } from 'reactflow' import type { CursorPosition, OnlineUser } from '@/app/components/workflow/collaboration/types' import { getUserColor } from '../utils/user-color' @@ -13,6 +14,15 @@ const UserCursors: FC = ({ myUserId, onlineUsers, }) => { + const viewport = useViewport() + + const convertToScreenCoordinates = (cursor: CursorPosition) => { + // Convert world coordinates to screen coordinates using current viewport + const screenX = cursor.x * viewport.zoom + viewport.x + const screenY = cursor.y * viewport.zoom + viewport.y + + return { x: screenX, y: screenY } + } return ( <> {Object.entries(cursors || {}).map(([userId, cursor]) => { @@ -22,14 +32,15 @@ const UserCursors: FC = ({ const userInfo = onlineUsers.find(user => user.user_id === userId) const userName = userInfo?.username || `User ${userId.slice(-4)}` const userColor = getUserColor(userId) + const screenPos = convertToScreenCoordinates(cursor) return (
) => { + const startCursorTracking = (containerRef: React.RefObject, reactFlowInstance?: ReactFlowInstance) => { if (cursorServiceRef.current) { cursorServiceRef.current.startTracking(containerRef, (position) => { collaborationManager.emitCursorMove(position) - }) + }, reactFlowInstance) } } diff --git a/web/app/components/workflow/collaboration/services/cursor-service.ts b/web/app/components/workflow/collaboration/services/cursor-service.ts index f33d595a4a..2a86e37697 100644 --- a/web/app/components/workflow/collaboration/services/cursor-service.ts +++ b/web/app/components/workflow/collaboration/services/cursor-service.ts @@ -1,5 +1,6 @@ import type { RefObject } from 'react' import type { CursorPosition } from '../types/collaboration' +import type { ReactFlowInstance } from 'reactflow' export type CursorServiceConfig = { minMoveDistance?: number @@ -8,6 +9,7 @@ export type CursorServiceConfig = { export class CursorService { private containerRef: RefObject | null = null + private reactFlowInstance: ReactFlowInstance | null = null private isTracking = false private onCursorUpdate: ((cursors: Record) => void) | null = null private onEmitPosition: ((position: CursorPosition) => void) | null = null @@ -25,11 +27,13 @@ export class CursorService { startTracking( containerRef: RefObject, onEmitPosition: (position: CursorPosition) => void, + reactFlowInstance?: ReactFlowInstance, ): void { if (this.isTracking) this.stopTracking() this.containerRef = containerRef this.onEmitPosition = onEmitPosition + this.reactFlowInstance = reactFlowInstance || null this.isTracking = true if (containerRef.current) @@ -41,6 +45,7 @@ export class CursorService { this.containerRef.current.removeEventListener('mousemove', this.handleMouseMove) this.containerRef = null + this.reactFlowInstance = null this.onEmitPosition = null this.isTracking = false this.lastPosition = null @@ -59,26 +64,34 @@ export class CursorService { if (!this.containerRef?.current || !this.onEmitPosition) return const rect = this.containerRef.current.getBoundingClientRect() - const x = event.clientX - rect.left - const y = event.clientY - rect.top + let x = event.clientX - rect.left + let y = event.clientY - rect.top - if (x >= 0 && y >= 0 && x <= rect.width && y <= rect.height) { - const now = Date.now() - const timeThrottled = now - this.lastEmitTime > this.config.throttleMs - const distanceThrottled = !this.lastPosition - || (Math.abs(x - this.lastPosition.x) > this.config.minMoveDistance - || Math.abs(y - this.lastPosition.y) > this.config.minMoveDistance) + // Transform coordinates to ReactFlow world coordinates if ReactFlow instance is available + if (this.reactFlowInstance) { + const viewport = this.reactFlowInstance.getViewport() + // Convert screen coordinates to world coordinates + // World coordinates = (screen coordinates - viewport translation) / zoom + x = (x - viewport.x) / viewport.zoom + y = (y - viewport.y) / viewport.zoom + } - if (timeThrottled && distanceThrottled) { - this.lastPosition = { x, y } - this.lastEmitTime = now - this.onEmitPosition({ - x, - y, - userId: '', - timestamp: now, - }) - } + // Always emit cursor position (remove boundary check since world coordinates can be negative) + const now = Date.now() + const timeThrottled = now - this.lastEmitTime > this.config.throttleMs + const distanceThrottled = !this.lastPosition + || (Math.abs(x - this.lastPosition.x) > this.config.minMoveDistance / (this.reactFlowInstance?.getZoom() || 1)) + || (Math.abs(y - this.lastPosition.y) > this.config.minMoveDistance / (this.reactFlowInstance?.getZoom() || 1)) + + if (timeThrottled && distanceThrottled) { + this.lastPosition = { x, y } + this.lastEmitTime = now + this.onEmitPosition({ + x, + y, + userId: '', + timestamp: now, + }) } } }