mirror of
https://github.com/langgenius/dify.git
synced 2026-01-08 07:14:14 +00:00
add online users backend api and frontend submit cursor pos
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
import json
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource, marshal_with, reqparse
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from extensions.ext_redis import redis_client
|
||||
from extensions.ext_socketio import ext_socketio
|
||||
from fields.online_user_fields import online_user_list_fields
|
||||
from libs.login import login_required
|
||||
|
||||
|
||||
@ext_socketio.on("user_connect")
|
||||
@@ -25,8 +30,8 @@ def handle_user_connect(data):
|
||||
|
||||
user_info = {
|
||||
"user_id": current_user.id,
|
||||
"username": getattr(current_user, "username", ""),
|
||||
"avatar": getattr(current_user, "avatar", ""),
|
||||
"username": current_user.name,
|
||||
"avatar": current_user.avatar,
|
||||
"sid": sid,
|
||||
}
|
||||
|
||||
@@ -49,3 +54,32 @@ def handle_disconnect():
|
||||
user_id = data["user_id"]
|
||||
redis_client.hdel(f"workflow_online_users:{workflow_id}", user_id)
|
||||
redis_client.delete(f"ws_sid_map:{sid}")
|
||||
|
||||
|
||||
class OnlineUserApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@marshal_with(online_user_list_fields)
|
||||
def get(self):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("workflow_ids", type=str, required=True, location="args")
|
||||
args = parser.parse_args()
|
||||
|
||||
workflow_ids = [id.strip() for id in args["workflow_ids"].split(",")]
|
||||
|
||||
results = {}
|
||||
for workflow_id in workflow_ids:
|
||||
users_json = redis_client.hgetall(f"workflow_online_users:{workflow_id}")
|
||||
|
||||
users = []
|
||||
for _, user_info_json in users_json.items():
|
||||
try:
|
||||
users.append(json.loads(user_info_json))
|
||||
except Exception:
|
||||
continue
|
||||
results[workflow_id] = users
|
||||
|
||||
return {"data": results}
|
||||
|
||||
api.add_resource(OnlineUserApi, "/online-users")
|
||||
|
||||
17
api/fields/online_user_fields.py
Normal file
17
api/fields/online_user_fields.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from flask_restful import fields
|
||||
|
||||
online_user_partial_fields = {
|
||||
"id": fields.String,
|
||||
"username": fields.String,
|
||||
"avatar": fields.String,
|
||||
"sid": fields.String,
|
||||
}
|
||||
|
||||
workflow_online_users_fields = {
|
||||
"workflow_id": fields.String,
|
||||
"users": fields.List(fields.Nested(online_user_partial_fields))
|
||||
}
|
||||
|
||||
online_user_list_fields = {
|
||||
"data": fields.List(fields.Nested(workflow_online_users_fields)),
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
useWorkflowStartRun,
|
||||
} from '../hooks'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { useCollaborativeCursors } from '../hooks'
|
||||
|
||||
type WorkflowMainProps = Pick<WorkflowProps, 'nodes' | 'edges' | 'viewport'>
|
||||
const WorkflowMain = ({
|
||||
@@ -65,6 +66,9 @@ const WorkflowMain = ({
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
} = useWorkflowStartRun()
|
||||
const appId = useStore(s => s.appId)
|
||||
|
||||
const { cursors, myUserId } = useCollaborativeCursors(appId)
|
||||
|
||||
const { fetchInspectVars } = useSetWorkflowVarsWithValue({
|
||||
flowId: appId,
|
||||
...useConfigsMap(),
|
||||
@@ -148,15 +152,53 @@ const WorkflowMain = ({
|
||||
])
|
||||
|
||||
return (
|
||||
<WorkflowWithInnerContext
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
viewport={viewport}
|
||||
onWorkflowDataUpdate={handleWorkflowDataUpdate}
|
||||
hooksStore={hooksStore}
|
||||
>
|
||||
<WorkflowChildren />
|
||||
</WorkflowWithInnerContext>
|
||||
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||
<WorkflowWithInnerContext
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
viewport={viewport}
|
||||
onWorkflowDataUpdate={handleWorkflowDataUpdate}
|
||||
hooksStore={hooksStore}
|
||||
>
|
||||
<WorkflowChildren />
|
||||
</WorkflowWithInnerContext>
|
||||
|
||||
{/* Render other users' cursors on top */}
|
||||
{Object.entries(cursors || {}).map(([userId, cursor]) => {
|
||||
if (userId === myUserId)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div
|
||||
key={userId}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: cursor.x,
|
||||
top: cursor.y,
|
||||
pointerEvents: 'none', // Important: allows clicking through the cursor
|
||||
zIndex: 9999, // Ensure cursors are on top of other elements
|
||||
transition: 'left 0.1s linear, top 0.1s linear', // Optional: for smoother movement
|
||||
}}
|
||||
>
|
||||
{/* You can replace this with your own cursor SVG or component */}
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.5 3.75L10.5 18.25L12.5 11.25L19.5 9.25L5.5 3.75Z" fill={cursor.color || 'black'} stroke="white" strokeWidth="1.5" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
<span style={{
|
||||
backgroundColor: cursor.color || 'black',
|
||||
color: 'white',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '12px',
|
||||
fontSize: '12px',
|
||||
whiteSpace: 'nowrap',
|
||||
marginLeft: '4px',
|
||||
}}>
|
||||
{cursor.name || userId}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from './use-workflow-refresh-draft'
|
||||
export * from '../../workflow/hooks/use-fetch-workflow-inspect-vars'
|
||||
export * from './use-inspect-vars-crud'
|
||||
export * from './use-configs-map'
|
||||
export * from './use-workflow-websocket'
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
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
|
||||
}
|
||||
|
||||
export function useCollaborativeCursors(appId: string) {
|
||||
const [cursors, setCursors] = useState<Record<string, Cursor>>({})
|
||||
const socketRef = useRef<ReturnType<typeof connectOnlineUserWebSocket> | null>(null)
|
||||
const lastSent = useRef<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
// Connect websocket
|
||||
const socket = connectOnlineUserWebSocket(appId)
|
||||
socketRef.current = socket
|
||||
|
||||
// Listen for other users' cursor updates
|
||||
socket.on('users_mouse_positions', (positions: Record<string, Cursor>) => {
|
||||
setCursors(positions)
|
||||
})
|
||||
|
||||
// Mouse move handler with throttle (e.g. 30ms)
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const now = Date.now()
|
||||
if (now - lastSent.current > 30) {
|
||||
socket.emit('mouse_move', { x: e.clientX, y: e.clientY })
|
||||
lastSent.current = now
|
||||
}
|
||||
}
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
socket.off('users_mouse_positions')
|
||||
disconnectOnlineUserWebSocket()
|
||||
}
|
||||
}, [appId])
|
||||
|
||||
return cursors
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type { Socket } from 'socket.io-client'
|
||||
import { io } from 'socket.io-client'
|
||||
|
||||
let socket: Socket | null = null
|
||||
let lastAppId: string | null = null
|
||||
|
||||
/**
|
||||
* Connect to the online user websocket server.
|
||||
@@ -10,7 +11,8 @@ let socket: Socket | null = null
|
||||
* @returns The socket instance.
|
||||
*/
|
||||
export function connectOnlineUserWebSocket(appId: string): Socket {
|
||||
// If already connected, disconnect first
|
||||
if (socket && lastAppId === appId)
|
||||
return socket
|
||||
if (socket)
|
||||
socket.disconnect()
|
||||
|
||||
@@ -24,6 +26,8 @@ export function connectOnlineUserWebSocket(appId: string): Socket {
|
||||
withCredentials: true,
|
||||
})
|
||||
|
||||
lastAppId = appId
|
||||
|
||||
// Add your event listeners here
|
||||
socket.on('connect', () => {
|
||||
socket?.emit('user_connect', { workflow_id: appId })
|
||||
|
||||
Reference in New Issue
Block a user