From f4438b0a08443e4d3e221c2d69c2d248995e671f Mon Sep 17 00:00:00 2001 From: hjlarry Date: Tue, 22 Jul 2025 18:08:35 +0800 Subject: [PATCH] support mouse display --- .../workflow-app/components/workflow-main.tsx | 151 ++++++++++++++---- .../hooks/use-workflow-websocket.ts | 87 ++++------ .../workflow/store/websocket-store.ts | 86 ++++++++++ 3 files changed, 237 insertions(+), 87 deletions(-) create mode 100644 web/app/components/workflow/store/websocket-store.ts diff --git a/web/app/components/workflow-app/components/workflow-main.tsx b/web/app/components/workflow-app/components/workflow-main.tsx index 30ec41ef35..5ed4b1b627 100644 --- a/web/app/components/workflow-app/components/workflow-main.tsx +++ b/web/app/components/workflow-app/components/workflow-main.tsx @@ -2,6 +2,8 @@ import { useCallback, useEffect, useMemo, + useRef, + useState, } from 'react' import { useFeaturesStore } from '@/app/components/base/features/hooks' import { WorkflowWithInnerContext } from '@/app/components/workflow' @@ -17,6 +19,7 @@ import { useWorkflowStartRun, } from '../hooks' import { useStore, useWorkflowStore } from '@/app/components/workflow/store' +import { useWebSocketStore } from '@/app/components/workflow/store/websocket-store' import { useCollaborativeCursors } from '../hooks' import { connectOnlineUserWebSocket } from '@/service/demo/online-user' import type { OnlineUser } from '@/service/demo/online-user' @@ -30,6 +33,12 @@ const WorkflowMain = ({ const featuresStore = useFeaturesStore() const workflowStore = useWorkflowStore() const appId = useStore(s => s.appId) + const containerRef = useRef(null) + const lastEmitTimeRef = useRef(0) + const lastPositionRef = useRef<{ x: number; y: number } | null>(null) + + // WebSocket connection for collaboration + const { emit } = useWebSocketStore() const handleWorkflowDataUpdate = useCallback((payload: any) => { const { @@ -52,6 +61,48 @@ const WorkflowMain = ({ } }, [featuresStore, workflowStore]) + // Handle mouse movement for collaboration with throttling (1 second) + const handleMouseMove = useCallback((event: MouseEvent) => { + if (!containerRef.current) return + + const rect = containerRef.current.getBoundingClientRect() + const x = event.clientX - rect.left + const y = event.clientY - rect.top + + // Only emit if mouse is within the container + if (x >= 0 && y >= 0 && x <= rect.width && y <= rect.height) { + const now = Date.now() + const timeSinceLastEmit = now - lastEmitTimeRef.current + + // Throttle to 1 second (1000ms) + if (timeSinceLastEmit >= 1000) { + lastEmitTimeRef.current = now + lastPositionRef.current = { x, y } + + emit('mouseMove', { + x, + y, + }) + } + else { + // Update position for potential future emit + lastPositionRef.current = { x, y } + } + } + }, [emit]) + + // Add mouse move event listener + useEffect(() => { + const container = containerRef.current + if (!container) return + + container.addEventListener('mousemove', handleMouseMove) + + return () => { + container.removeEventListener('mousemove', handleMouseMove) + } + }, [handleMouseMove]) + const { doSyncWorkflowDraft, syncWorkflowDraftWhenPageClose, @@ -71,30 +122,22 @@ const WorkflowMain = ({ } = useWorkflowStartRun() const { cursors, myUserId } = useCollaborativeCursors(appId) + const [onlineUsers, setOnlineUsers] = useState>({}) - // Add online users logging useEffect(() => { if (!appId) return - - // Connect to WebSocket for online users const socket = connectOnlineUserWebSocket(appId) - // Handle online users update const handleOnlineUsersUpdate = (data: { users: OnlineUser[] }) => { - data.users.forEach((user) => { - console.log(`👤 User: ${user.username} (ID: ${user.user_id})`) - }) + const usersMap = data.users.reduce((acc, user) => { + acc[user.user_id] = user + return acc + }, {} as Record) + setOnlineUsers(usersMap) } - - // Add event listeners socket.on('online_users', handleOnlineUsersUpdate) - - // Log initial connection - console.log('🔌 Connecting to online users WebSocket for app:', appId) - - // Cleanup function + // clean up return () => { - console.log(' Cleaning up online users WebSocket listeners') socket.off('online_users', handleOnlineUsersUpdate) } }, [appId]) @@ -182,7 +225,10 @@ const WorkflowMain = ({ ]) return ( -
+
{ + const colors = ['#3B82F6', '#EF4444', '#10B981', '#F59E0B', '#8B5CF6', '#EC4899', '#06B6D4', '#84CC16'] + const hash = id.split('').reduce((a, b) => { + a = ((a << 5) - a) + b.charCodeAt(0) + return a & a + }, 0) + return colors[Math.abs(hash) % colors.length] + } + + const userColor = getUserColor(userId) + return (
- {/* You can replace this with your own cursor SVG or component */} - - + + - - {cursor.name || userId} - + +
+ {userName} +
) })} diff --git a/web/app/components/workflow-app/hooks/use-workflow-websocket.ts b/web/app/components/workflow-app/hooks/use-workflow-websocket.ts index 1980e92e8a..c615c2a4db 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-websocket.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-websocket.ts @@ -1,69 +1,48 @@ -import { - useEffect, - useRef, - useState, -} from 'react' - -import { connectOnlineUserWebSocket, disconnectOnlineUserWebSocket } from '@/service/demo/online-user' - -type Cursor = { - x: number - y: number - userId: string - name?: string - color?: string -} +import { useEffect, useState } from 'react' +import { useWebSocketStore } from '@/app/components/workflow/store/websocket-store' export function useCollaborativeCursors(appId: string) { - const [cursors, setCursors] = useState>({}) - const socketRef = useRef | null>(null) - const lastSent = useRef(0) + const { on, connect, disconnect } = useWebSocketStore() + const [cursors, setCursors] = useState>({}) + const [myUserId, setMyUserId] = useState(null) useEffect(() => { - // Connect websocket - const socket = connectOnlineUserWebSocket(appId) - socketRef.current = socket + if (!appId) return + connect(appId) - // Listen for collaboration updates from other users - socket.on('collaboration_update', (update: { - type: string - userId: string - data: any - timestamp: number - }) => { - if (update.type === 'mouseMove') { + return () => { + disconnect() + } + }, [appId, connect, disconnect]) + + useEffect(() => { + const unsubscribe = on('mouseMove', (update) => { + const userId = update.userId || update.user_id + const data = update.data || update + + if (userId && data) { setCursors(prev => ({ ...prev, - [update.userId]: { - x: update.data.x, - y: update.data.y, - userId: update.userId, + [userId]: { + x: data.x, + y: data.y, + userId, }, })) } - // if (update.type === 'openPanel') { ... } }) - // Mouse move handler with throttle 300ms - const handleMouseMove = (e: MouseEvent) => { - const now = Date.now() - if (now - lastSent.current > 300) { - socket.emit('collaboration_event', { - type: 'mouseMove', - data: { x: e.clientX, y: e.clientY }, - timestamp: now, - }) - lastSent.current = now - } - } - window.addEventListener('mousemove', handleMouseMove) + return unsubscribe + }, [on]) - return () => { - window.removeEventListener('mousemove', handleMouseMove) - socket.off('collaboration_update') - disconnectOnlineUserWebSocket() - } - }, [appId]) + useEffect(() => { + const unsubscribe = on('connected', (data) => { + if (data.userId || data.user_id) + setMyUserId(data.userId || data.user_id) + }) - return cursors + return unsubscribe + }, [on]) + + return { cursors, myUserId } } diff --git a/web/app/components/workflow/store/websocket-store.ts b/web/app/components/workflow/store/websocket-store.ts new file mode 100644 index 0000000000..f6288e6603 --- /dev/null +++ b/web/app/components/workflow/store/websocket-store.ts @@ -0,0 +1,86 @@ +import { create } from 'zustand' +import { connectOnlineUserWebSocket, disconnectOnlineUserWebSocket } from '@/service/demo/online-user' + +type WebSocketInstance = ReturnType + +type WebSocketStore = { + socket: WebSocketInstance | null + isConnected: boolean + listeners: Map void>> + + // Actions + connect: (appId: string) => void + disconnect: () => void + emit: (eventType: string, data: any) => void + on: (eventType: string, handler: (data: any) => void) => () => void +} + +export const useWebSocketStore = create((set, get) => ({ + socket: null, + isConnected: false, + listeners: new Map(), + + connect: (appId: string) => { + const socket = connectOnlineUserWebSocket(appId) + + socket.on('collaboration_update', (update: { + type: string + userId: string + data: any + timestamp: number + }) => { + const { listeners } = get() + const eventListeners = listeners.get(update.type) + if (eventListeners) { + eventListeners.forEach((handler) => { + try { + handler(update) + } + catch (error) { + console.error(`Error in collaboration event handler for ${update.type}:`, error) + } + }) + } + }) + + set({ socket, isConnected: true }) + }, + + disconnect: () => { + const { socket } = get() + if (socket) { + socket.off('collaboration_update') + disconnectOnlineUserWebSocket() + } + set({ socket: null, isConnected: false, listeners: new Map() }) + }, + + emit: (eventType: string, data: any) => { + const { socket, isConnected } = get() + if (socket && isConnected) { + socket.emit('collaboration_event', { + type: eventType, + data, + timestamp: Date.now(), + }) + } + }, + + on: (eventType: string, handler: (data: any) => void) => { + const { listeners } = get() + + if (!listeners.has(eventType)) + listeners.set(eventType, new Set()) + + listeners.get(eventType)!.add(handler) + + return () => { + const currentListeners = get().listeners.get(eventType) + if (currentListeners) { + currentListeners.delete(handler) + if (currentListeners.size === 0) + get().listeners.delete(eventType) + } + } + }, +}))