mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 18:08:07 +08:00
fix web style
This commit is contained in:
@ -35,7 +35,7 @@ const NodeSelectorWrapper = (props: NodeSelectorProps) => {
|
||||
|
||||
return true
|
||||
})
|
||||
}, [availableNodesMetaData?.nodes])
|
||||
}, [availableNodesMetaData?.nodes]) as NodeSelectorProps['blocks']
|
||||
|
||||
return (
|
||||
<NodeSelector
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { Value } from 'loro-crdt'
|
||||
import type { Socket } from 'socket.io-client'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
@ -24,18 +25,46 @@ type NodePanelPresenceEventData = {
|
||||
action: 'open' | 'close'
|
||||
user: NodePanelPresenceUser
|
||||
clientId: string
|
||||
timestamp?: number
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
type ReactFlowStore = {
|
||||
getState: () => {
|
||||
getNodes: () => Node[]
|
||||
setNodes: (nodes: Node[]) => void
|
||||
getEdges: () => Edge[]
|
||||
setEdges: (edges: Edge[]) => void
|
||||
}
|
||||
}
|
||||
|
||||
type CollaborationEventPayload = {
|
||||
type: CollaborationUpdate['type']
|
||||
data: Record<string, unknown>
|
||||
timestamp: number
|
||||
userId?: string
|
||||
}
|
||||
|
||||
type LoroSubscribeEvent = {
|
||||
by?: string
|
||||
}
|
||||
|
||||
type LoroContainer = {
|
||||
kind?: () => string
|
||||
getAttached?: () => unknown
|
||||
}
|
||||
|
||||
const toLoroValue = (value: unknown): Value => cloneDeep(value) as Value
|
||||
const toLoroRecord = (value: unknown): Record<string, Value> => cloneDeep(value) as Record<string, Value>
|
||||
|
||||
export class CollaborationManager {
|
||||
private doc: LoroDoc | null = null
|
||||
private undoManager: UndoManager | null = null
|
||||
private provider: CRDTProvider | null = null
|
||||
private nodesMap: LoroMap | null = null
|
||||
private edgesMap: LoroMap | null = null
|
||||
private nodesMap: LoroMap<Record<string, Value>> | null = null
|
||||
private edgesMap: LoroMap<Record<string, Value>> | null = null
|
||||
private eventEmitter = new EventEmitter()
|
||||
private currentAppId: string | null = null
|
||||
private reactFlowStore: any = null
|
||||
private reactFlowStore: ReactFlowStore | null = null
|
||||
private isLeader = false
|
||||
private leaderId: string | null = null
|
||||
private cursors: Record<string, CursorPosition> = {}
|
||||
@ -80,7 +109,7 @@ export class CollaborationManager {
|
||||
)
|
||||
}
|
||||
|
||||
private sendCollaborationEvent(payload: any): void {
|
||||
private sendCollaborationEvent(payload: CollaborationEventPayload): void {
|
||||
const socket = this.getActiveSocket()
|
||||
if (!socket)
|
||||
return
|
||||
@ -88,7 +117,7 @@ export class CollaborationManager {
|
||||
emitWithAuthGuard(socket, 'collaboration_event', payload, { onUnauthorized: this.handleSessionUnauthorized })
|
||||
}
|
||||
|
||||
private sendGraphEvent(payload: any): void {
|
||||
private sendGraphEvent(payload: Uint8Array): void {
|
||||
const socket = this.getActiveSocket()
|
||||
if (!socket)
|
||||
return
|
||||
@ -96,59 +125,67 @@ export class CollaborationManager {
|
||||
emitWithAuthGuard(socket, 'graph_event', payload, { onUnauthorized: this.handleSessionUnauthorized })
|
||||
}
|
||||
|
||||
private getNodeContainer(nodeId: string): LoroMap<any> {
|
||||
private getNodeContainer(nodeId: string): LoroMap<Record<string, Value>> {
|
||||
if (!this.nodesMap)
|
||||
throw new Error('Nodes map not initialized')
|
||||
|
||||
let container = this.nodesMap.get(nodeId) as any
|
||||
let container = this.nodesMap.get(nodeId) as unknown
|
||||
|
||||
if (!container || typeof container.kind !== 'function' || container.kind() !== 'Map') {
|
||||
const isMapContainer = (value: unknown): value is LoroMap<Record<string, Value>> & LoroContainer => {
|
||||
return !!value && typeof (value as LoroContainer).kind === 'function' && (value as LoroContainer).kind?.() === 'Map'
|
||||
}
|
||||
|
||||
if (!container || !isMapContainer(container)) {
|
||||
const previousValue = container
|
||||
const newContainer = this.nodesMap.setContainer(nodeId, new LoroMap())
|
||||
container = typeof newContainer.getAttached === 'function' ? newContainer.getAttached() ?? newContainer : newContainer
|
||||
const attached = (newContainer as LoroContainer).getAttached?.() ?? newContainer
|
||||
container = attached
|
||||
if (previousValue && typeof previousValue === 'object')
|
||||
this.populateNodeContainer(container, previousValue as Node)
|
||||
this.populateNodeContainer(container as LoroMap<Record<string, Value>>, previousValue as Node)
|
||||
}
|
||||
else {
|
||||
container = typeof container.getAttached === 'function' ? container.getAttached() ?? container : container
|
||||
const attached = (container as LoroContainer).getAttached?.() ?? container
|
||||
container = attached
|
||||
}
|
||||
|
||||
return container
|
||||
return container as LoroMap<Record<string, Value>>
|
||||
}
|
||||
|
||||
private ensureDataContainer(nodeContainer: LoroMap<any>): LoroMap<any> {
|
||||
let dataContainer = nodeContainer.get('data') as any
|
||||
private ensureDataContainer(nodeContainer: LoroMap<Record<string, Value>>): LoroMap<Record<string, Value>> {
|
||||
let dataContainer = nodeContainer.get('data') as unknown
|
||||
|
||||
if (!dataContainer || typeof dataContainer.kind !== 'function' || dataContainer.kind() !== 'Map')
|
||||
if (!dataContainer || typeof (dataContainer as LoroContainer).kind !== 'function' || (dataContainer as LoroContainer).kind?.() !== 'Map')
|
||||
dataContainer = nodeContainer.setContainer('data', new LoroMap())
|
||||
|
||||
return typeof dataContainer.getAttached === 'function' ? dataContainer.getAttached() ?? dataContainer : dataContainer
|
||||
const attached = (dataContainer as LoroContainer).getAttached?.() ?? dataContainer
|
||||
return attached as LoroMap<Record<string, Value>>
|
||||
}
|
||||
|
||||
private ensureList(nodeContainer: LoroMap<any>, key: string): LoroList<any> {
|
||||
private ensureList(nodeContainer: LoroMap<Record<string, Value>>, key: string): LoroList<unknown> {
|
||||
const dataContainer = this.ensureDataContainer(nodeContainer)
|
||||
let list = dataContainer.get(key) as any
|
||||
let list = dataContainer.get(key) as unknown
|
||||
|
||||
if (!list || typeof list.kind !== 'function' || list.kind() !== 'List')
|
||||
if (!list || typeof (list as LoroContainer).kind !== 'function' || (list as LoroContainer).kind?.() !== 'List')
|
||||
list = dataContainer.setContainer(key, new LoroList())
|
||||
|
||||
return typeof list.getAttached === 'function' ? list.getAttached() ?? list : list
|
||||
const attached = (list as LoroContainer).getAttached?.() ?? list
|
||||
return attached as LoroList<unknown>
|
||||
}
|
||||
|
||||
private exportNode(nodeId: string): Node {
|
||||
const container = this.getNodeContainer(nodeId)
|
||||
const json = container.toJSON() as any
|
||||
const json = container.toJSON() as Node
|
||||
return {
|
||||
...json,
|
||||
data: json.data || {},
|
||||
}
|
||||
}
|
||||
|
||||
private populateNodeContainer(container: LoroMap<any>, node: Node): void {
|
||||
private populateNodeContainer(container: LoroMap<Record<string, Value>>, node: Node): void {
|
||||
const listFields = new Set(['variables', 'prompt_template', 'parameters'])
|
||||
container.set('id', node.id)
|
||||
container.set('type', node.type)
|
||||
container.set('position', cloneDeep(node.position))
|
||||
container.set('position', toLoroValue(node.position))
|
||||
container.set('sourcePosition', node.sourcePosition)
|
||||
container.set('targetPosition', node.targetPosition)
|
||||
|
||||
@ -189,7 +226,7 @@ export class CollaborationManager {
|
||||
if (value === undefined)
|
||||
container.delete(prop as string)
|
||||
else
|
||||
container.set(prop as string, cloneDeep(value as any))
|
||||
container.set(prop as string, toLoroValue(value))
|
||||
})
|
||||
|
||||
const dataContainer = this.ensureDataContainer(container)
|
||||
@ -203,7 +240,7 @@ export class CollaborationManager {
|
||||
if (listFields.has(key))
|
||||
this.syncList(container, key, Array.isArray(value) ? value : [])
|
||||
else
|
||||
dataContainer.set(key, cloneDeep(value))
|
||||
dataContainer.set(key, toLoroValue(value))
|
||||
})
|
||||
|
||||
const existingData = dataContainer.toJSON() || {}
|
||||
@ -222,9 +259,9 @@ export class CollaborationManager {
|
||||
return (syncDataAllowList.has(key) || !key.startsWith('_')) && key !== 'selected'
|
||||
}
|
||||
|
||||
private syncList(nodeContainer: LoroMap<any>, key: string, desired: any[]): void {
|
||||
private syncList(nodeContainer: LoroMap<Record<string, Value>>, key: string, desired: Array<unknown>): void {
|
||||
const list = this.ensureList(nodeContainer, key)
|
||||
const current = list.toJSON() as any[]
|
||||
const current = list.toJSON() as Array<unknown>
|
||||
const target = Array.isArray(desired) ? desired : []
|
||||
const minLength = Math.min(current.length, target.length)
|
||||
|
||||
@ -309,7 +346,7 @@ export class CollaborationManager {
|
||||
this.eventEmitter.emit('nodePanelPresence', this.getNodePanelPresenceSnapshot())
|
||||
}
|
||||
|
||||
init = (appId: string, reactFlowStore: any): void => {
|
||||
init = (appId: string, reactFlowStore: ReactFlowStore): void => {
|
||||
if (!reactFlowStore) {
|
||||
console.warn('CollaborationManager.init called without reactFlowStore, deferring to connect()')
|
||||
return
|
||||
@ -345,7 +382,7 @@ export class CollaborationManager {
|
||||
this.disconnect()
|
||||
}
|
||||
|
||||
async connect(appId: string, reactFlowStore?: any): Promise<string> {
|
||||
async connect(appId: string, reactFlowStore?: ReactFlowStore): Promise<string> {
|
||||
const connectionId = Math.random().toString(36).substring(2, 11)
|
||||
|
||||
this.activeConnections.add(connectionId)
|
||||
@ -373,15 +410,15 @@ export class CollaborationManager {
|
||||
this.setupSocketEventListeners(socket)
|
||||
|
||||
this.doc = new LoroDoc()
|
||||
this.nodesMap = this.doc.getMap('nodes')
|
||||
this.edgesMap = this.doc.getMap('edges')
|
||||
this.nodesMap = this.doc.getMap('nodes') as LoroMap<Record<string, Value>>
|
||||
this.edgesMap = this.doc.getMap('edges') as LoroMap<Record<string, Value>>
|
||||
|
||||
// Initialize UndoManager for collaborative undo/redo
|
||||
this.undoManager = new UndoManager(this.doc, {
|
||||
maxUndoSteps: 100,
|
||||
mergeInterval: 500, // Merge operations within 500ms
|
||||
excludeOriginPrefixes: [], // Don't exclude anything - let UndoManager track all local operations
|
||||
onPush: (isUndo, range, event) => {
|
||||
onPush: (_isUndo, _range, _event) => {
|
||||
// Store current selection state when an operation is pushed
|
||||
const selectedNode = this.reactFlowStore?.getState().getNodes().find((n: Node) => n.data?.selected)
|
||||
|
||||
@ -401,10 +438,10 @@ export class CollaborationManager {
|
||||
cursors: [],
|
||||
}
|
||||
},
|
||||
onPop: (isUndo, value, counterRange) => {
|
||||
onPop: (_isUndo, value, _counterRange) => {
|
||||
// Restore selection state when undoing/redoing
|
||||
if (value?.value && typeof value.value === 'object' && 'selectedNodeId' in value.value && this.reactFlowStore) {
|
||||
const selectedNodeId = (value.value as any).selectedNodeId
|
||||
const selectedNodeId = (value.value as { selectedNodeId?: string | null }).selectedNodeId
|
||||
if (selectedNodeId) {
|
||||
const { setNodes } = this.reactFlowStore.getState()
|
||||
const nodes = this.reactFlowStore.getState().getNodes()
|
||||
@ -481,7 +518,7 @@ export class CollaborationManager {
|
||||
}
|
||||
|
||||
getEdges(): Edge[] {
|
||||
return this.edgesMap ? Array.from(this.edgesMap.values()) : []
|
||||
return this.edgesMap ? Array.from(this.edgesMap.values()) as Edge[] : []
|
||||
}
|
||||
|
||||
emitCursorMove(position: CursorPosition): void {
|
||||
@ -567,23 +604,23 @@ export class CollaborationManager {
|
||||
return this.eventEmitter.on('workflowUpdate', callback)
|
||||
}
|
||||
|
||||
onVarsAndFeaturesUpdate(callback: (update: any) => void): () => void {
|
||||
onVarsAndFeaturesUpdate(callback: (update: CollaborationUpdate) => void): () => void {
|
||||
return this.eventEmitter.on('varsAndFeaturesUpdate', callback)
|
||||
}
|
||||
|
||||
onAppStateUpdate(callback: (update: any) => void): () => void {
|
||||
onAppStateUpdate(callback: (update: CollaborationUpdate) => void): () => void {
|
||||
return this.eventEmitter.on('appStateUpdate', callback)
|
||||
}
|
||||
|
||||
onAppPublishUpdate(callback: (update: any) => void): () => void {
|
||||
onAppPublishUpdate(callback: (update: CollaborationUpdate) => void): () => void {
|
||||
return this.eventEmitter.on('appPublishUpdate', callback)
|
||||
}
|
||||
|
||||
onAppMetaUpdate(callback: (update: any) => void): () => void {
|
||||
onAppMetaUpdate(callback: (update: CollaborationUpdate) => void): () => void {
|
||||
return this.eventEmitter.on('appMetaUpdate', callback)
|
||||
}
|
||||
|
||||
onMcpServerUpdate(callback: (update: any) => void): () => void {
|
||||
onMcpServerUpdate(callback: (update: CollaborationUpdate) => void): () => void {
|
||||
return this.eventEmitter.on('mcpServerUpdate', callback)
|
||||
}
|
||||
|
||||
@ -635,12 +672,13 @@ export class CollaborationManager {
|
||||
const result = this.undoManager.undo()
|
||||
|
||||
// After undo, manually update React state from CRDT without triggering collaboration
|
||||
if (result && this.reactFlowStore) {
|
||||
const reactFlowStore = this.reactFlowStore
|
||||
if (result && reactFlowStore) {
|
||||
requestAnimationFrame(() => {
|
||||
// Get ReactFlow's native setters, not the collaborative ones
|
||||
const state = this.reactFlowStore.getState()
|
||||
const updatedNodes = Array.from(this.nodesMap?.values() || [])
|
||||
const updatedEdges = Array.from(this.edgesMap?.values() || [])
|
||||
const state = reactFlowStore.getState()
|
||||
const updatedNodes = Array.from(this.nodesMap?.values() || []) as Node[]
|
||||
const updatedEdges = Array.from(this.edgesMap?.values() || []) as Edge[]
|
||||
// Call ReactFlow's native setters directly to avoid triggering collaboration
|
||||
state.setNodes(updatedNodes)
|
||||
state.setEdges(updatedEdges)
|
||||
@ -674,12 +712,13 @@ export class CollaborationManager {
|
||||
const result = this.undoManager.redo()
|
||||
|
||||
// After redo, manually update React state from CRDT without triggering collaboration
|
||||
if (result && this.reactFlowStore) {
|
||||
const reactFlowStore = this.reactFlowStore
|
||||
if (result && reactFlowStore) {
|
||||
requestAnimationFrame(() => {
|
||||
// Get ReactFlow's native setters, not the collaborative ones
|
||||
const state = this.reactFlowStore.getState()
|
||||
const updatedNodes = Array.from(this.nodesMap?.values() || [])
|
||||
const updatedEdges = Array.from(this.edgesMap?.values() || [])
|
||||
const state = reactFlowStore.getState()
|
||||
const updatedNodes = Array.from(this.nodesMap?.values() || []) as Node[]
|
||||
const updatedEdges = Array.from(this.edgesMap?.values() || []) as Edge[]
|
||||
// Call ReactFlow's native setters directly to avoid triggering collaboration
|
||||
state.setNodes(updatedNodes)
|
||||
state.setEdges(updatedEdges)
|
||||
@ -753,21 +792,22 @@ export class CollaborationManager {
|
||||
newEdges.forEach((newEdge) => {
|
||||
const oldEdge = oldEdgesMap.get(newEdge.id)
|
||||
if (!oldEdge || !isEqual(oldEdge, newEdge)) {
|
||||
const clonedEdge = cloneDeep(newEdge)
|
||||
const clonedEdge = toLoroRecord(newEdge)
|
||||
this.edgesMap?.set(newEdge.id, clonedEdge)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private setupSubscriptions(): void {
|
||||
this.nodesMap?.subscribe((event: any) => {
|
||||
if (event.by === 'import' && this.reactFlowStore) {
|
||||
this.nodesMap?.subscribe((event: LoroSubscribeEvent) => {
|
||||
const reactFlowStore = this.reactFlowStore
|
||||
if (event.by === 'import' && reactFlowStore) {
|
||||
// Don't update React nodes during undo/redo to prevent loops
|
||||
if (this.isUndoRedoInProgress)
|
||||
return
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const state = this.reactFlowStore.getState()
|
||||
const state = reactFlowStore.getState()
|
||||
const previousNodes: Node[] = state.getNodes()
|
||||
const previousNodeMap = new Map(previousNodes.map(node => [node.id, node]))
|
||||
const selectedIds = new Set(
|
||||
@ -813,16 +853,17 @@ export class CollaborationManager {
|
||||
}
|
||||
})
|
||||
|
||||
this.edgesMap?.subscribe((event: any) => {
|
||||
if (event.by === 'import' && this.reactFlowStore) {
|
||||
this.edgesMap?.subscribe((event: LoroSubscribeEvent) => {
|
||||
const reactFlowStore = this.reactFlowStore
|
||||
if (event.by === 'import' && reactFlowStore) {
|
||||
// Don't update React edges during undo/redo to prevent loops
|
||||
if (this.isUndoRedoInProgress)
|
||||
return
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
// Get ReactFlow's native setters, not the collaborative ones
|
||||
const state = this.reactFlowStore.getState()
|
||||
const updatedEdges = Array.from(this.edgesMap?.values() || [])
|
||||
const state = reactFlowStore.getState()
|
||||
const updatedEdges = Array.from(this.edgesMap?.values() || []) as Edge[]
|
||||
|
||||
this.pendingInitialSync = false
|
||||
|
||||
@ -926,9 +967,6 @@ export class CollaborationManager {
|
||||
const wasLeader = this.isLeader
|
||||
this.isLeader = data.isLeader
|
||||
|
||||
if (wasLeader !== this.isLeader)
|
||||
console.log(`Collaboration: I am now the ${this.isLeader ? 'Leader' : 'Follower'}.`)
|
||||
|
||||
if (this.isLeader)
|
||||
this.pendingInitialSync = false
|
||||
else
|
||||
@ -943,13 +981,11 @@ export class CollaborationManager {
|
||||
})
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('WebSocket connected successfully')
|
||||
this.eventEmitter.emit('stateChange', { isConnected: true })
|
||||
this.pendingInitialSync = true
|
||||
})
|
||||
|
||||
socket.on('disconnect', (reason: string) => {
|
||||
console.log('WebSocket disconnected:', reason)
|
||||
socket.on('disconnect', () => {
|
||||
this.cursors = {}
|
||||
this.isLeader = false
|
||||
this.leaderId = null
|
||||
@ -958,12 +994,12 @@ export class CollaborationManager {
|
||||
this.eventEmitter.emit('cursors', {})
|
||||
})
|
||||
|
||||
socket.on('connect_error', (error: any) => {
|
||||
socket.on('connect_error', (error: Error) => {
|
||||
console.error('WebSocket connection error:', error)
|
||||
this.eventEmitter.emit('stateChange', { isConnected: false, error: error.message })
|
||||
})
|
||||
|
||||
socket.on('error', (error: any) => {
|
||||
socket.on('error', (error: Error) => {
|
||||
console.error('WebSocket error:', error)
|
||||
})
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ export class CRDTProvider {
|
||||
}
|
||||
|
||||
private setupEventListeners(): void {
|
||||
this.doc.subscribe((event: any) => {
|
||||
this.doc.subscribe((event: { by?: string }) => {
|
||||
if (event.by === 'local') {
|
||||
const update = this.doc.export({ mode: 'update' })
|
||||
emitWithAuthGuard(this.socket, 'graph_event', update, { onUnauthorized: this.onUnauthorized })
|
||||
|
||||
@ -1,24 +1,24 @@
|
||||
export type EventHandler<T = any> = (data: T) => void
|
||||
export type EventHandler<T = unknown> = (data: T) => void
|
||||
|
||||
export class EventEmitter {
|
||||
private events: Map<string, Set<EventHandler>> = new Map()
|
||||
private events: Map<string, Set<EventHandler<unknown>>> = new Map()
|
||||
|
||||
on<T = any>(event: string, handler: EventHandler<T>): () => void {
|
||||
on<T = unknown>(event: string, handler: EventHandler<T>): () => void {
|
||||
if (!this.events.has(event))
|
||||
this.events.set(event, new Set())
|
||||
|
||||
this.events.get(event)!.add(handler)
|
||||
this.events.get(event)!.add(handler as EventHandler<unknown>)
|
||||
|
||||
return () => this.off(event, handler)
|
||||
}
|
||||
|
||||
off<T = any>(event: string, handler?: EventHandler<T>): void {
|
||||
off<T = unknown>(event: string, handler?: EventHandler<T>): void {
|
||||
if (!this.events.has(event))
|
||||
return
|
||||
|
||||
const handlers = this.events.get(event)!
|
||||
if (handler)
|
||||
handlers.delete(handler)
|
||||
handlers.delete(handler as EventHandler<unknown>)
|
||||
else
|
||||
handlers.clear()
|
||||
|
||||
@ -26,7 +26,7 @@ export class EventEmitter {
|
||||
this.events.delete(event)
|
||||
}
|
||||
|
||||
emit<T = any>(event: string, data: T): void {
|
||||
emit<T = unknown>(event: string, data: T): void {
|
||||
if (!this.events.has(event))
|
||||
return
|
||||
|
||||
|
||||
@ -3,27 +3,31 @@ import type { DebugInfo, WebSocketConfig } from '../types/websocket'
|
||||
import { io } from 'socket.io-client'
|
||||
import { ACCESS_TOKEN_LOCAL_STORAGE_NAME } from '@/config'
|
||||
|
||||
const isUnauthorizedAck = (...ackArgs: any[]): boolean => {
|
||||
type AckArgs = unknown[]
|
||||
|
||||
const isUnauthorizedAck = (...ackArgs: AckArgs): boolean => {
|
||||
const [first, second] = ackArgs
|
||||
|
||||
if (second === 401 || first === 401)
|
||||
return true
|
||||
|
||||
if (first && typeof first === 'object' && first.msg === 'unauthorized')
|
||||
return true
|
||||
if (first && typeof first === 'object' && 'msg' in first) {
|
||||
const message = (first as { msg?: unknown }).msg
|
||||
return message === 'unauthorized'
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export type EmitAckOptions = {
|
||||
onAck?: (...ackArgs: any[]) => void
|
||||
onUnauthorized?: (...ackArgs: any[]) => void
|
||||
onAck?: (...ackArgs: AckArgs) => void
|
||||
onUnauthorized?: (...ackArgs: AckArgs) => void
|
||||
}
|
||||
|
||||
export const emitWithAuthGuard = (
|
||||
socket: Socket | null | undefined,
|
||||
event: string,
|
||||
payload: any,
|
||||
payload: unknown,
|
||||
options?: EmitAckOptions,
|
||||
): void => {
|
||||
if (!socket)
|
||||
@ -32,7 +36,7 @@ export const emitWithAuthGuard = (
|
||||
socket.emit(
|
||||
event,
|
||||
payload,
|
||||
(...ackArgs: any[]) => {
|
||||
(...ackArgs: AckArgs) => {
|
||||
options?.onAck?.(...ackArgs)
|
||||
if (isUnauthorizedAck(...ackArgs))
|
||||
options?.onUnauthorized?.(...ackArgs)
|
||||
|
||||
@ -1,31 +1,44 @@
|
||||
import type { ReactFlowInstance } from 'reactflow'
|
||||
import type { CollaborationState } from '../types/collaboration'
|
||||
import type {
|
||||
CollaborationState,
|
||||
CursorPosition,
|
||||
NodePanelPresenceMap,
|
||||
OnlineUser,
|
||||
} from '../types/collaboration'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { collaborationManager } from '../core/collaboration-manager'
|
||||
import { CursorService } from '../services/cursor-service'
|
||||
|
||||
export function useCollaboration(appId: string, reactFlowStore?: any) {
|
||||
const [state, setState] = useState<Partial<CollaborationState & { isLeader: boolean }>>({
|
||||
isConnected: false,
|
||||
onlineUsers: [],
|
||||
cursors: {},
|
||||
nodePanelPresence: {},
|
||||
isLeader: false,
|
||||
})
|
||||
type CollaborationViewState = {
|
||||
isConnected: boolean
|
||||
onlineUsers: OnlineUser[]
|
||||
cursors: Record<string, CursorPosition>
|
||||
nodePanelPresence: NodePanelPresenceMap
|
||||
isLeader: boolean
|
||||
}
|
||||
|
||||
type ReactFlowStore = NonNullable<Parameters<typeof collaborationManager.connect>[1]>
|
||||
|
||||
const initialState: CollaborationViewState = {
|
||||
isConnected: false,
|
||||
onlineUsers: [],
|
||||
cursors: {},
|
||||
nodePanelPresence: {},
|
||||
isLeader: false,
|
||||
}
|
||||
|
||||
export function useCollaboration(appId: string, reactFlowStore?: ReactFlowStore) {
|
||||
const [state, setState] = useState<CollaborationViewState>(initialState)
|
||||
|
||||
const cursorServiceRef = useRef<CursorService | null>(null)
|
||||
const isCollaborationEnabled = useGlobalPublicStore(s => s.systemFeatures.enable_collaboration_mode)
|
||||
|
||||
useEffect(() => {
|
||||
if (!appId || !isCollaborationEnabled) {
|
||||
setState({
|
||||
isConnected: false,
|
||||
onlineUsers: [],
|
||||
cursors: {},
|
||||
nodePanelPresence: {},
|
||||
isLeader: false,
|
||||
Promise.resolve().then(() => {
|
||||
setState(initialState)
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -44,7 +57,7 @@ export function useCollaboration(appId: string, reactFlowStore?: any) {
|
||||
return
|
||||
}
|
||||
connectionId = id
|
||||
setState((prev: any) => ({ ...prev, appId, isConnected: collaborationManager.isConnected() }))
|
||||
setState(prev => ({ ...prev, isConnected: collaborationManager.isConnected() }))
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to initialize collaboration:', error)
|
||||
@ -53,27 +66,27 @@ export function useCollaboration(appId: string, reactFlowStore?: any) {
|
||||
|
||||
initCollaboration()
|
||||
|
||||
const unsubscribeStateChange = collaborationManager.onStateChange((newState: any) => {
|
||||
console.log('Collaboration state change:', newState)
|
||||
setState((prev: any) => ({ ...prev, ...newState }))
|
||||
const unsubscribeStateChange = collaborationManager.onStateChange((newState: Partial<CollaborationState>) => {
|
||||
if (newState.isConnected === undefined)
|
||||
return
|
||||
|
||||
setState(prev => ({ ...prev, isConnected: newState.isConnected ?? prev.isConnected }))
|
||||
})
|
||||
|
||||
const unsubscribeCursors = collaborationManager.onCursorUpdate((cursors: any) => {
|
||||
setState((prev: any) => ({ ...prev, cursors }))
|
||||
const unsubscribeCursors = collaborationManager.onCursorUpdate((cursors: Record<string, CursorPosition>) => {
|
||||
setState(prev => ({ ...prev, cursors }))
|
||||
})
|
||||
|
||||
const unsubscribeUsers = collaborationManager.onOnlineUsersUpdate((users: any) => {
|
||||
console.log('Online users update:', users)
|
||||
setState((prev: any) => ({ ...prev, onlineUsers: users }))
|
||||
const unsubscribeUsers = collaborationManager.onOnlineUsersUpdate((users: OnlineUser[]) => {
|
||||
setState(prev => ({ ...prev, onlineUsers: users }))
|
||||
})
|
||||
|
||||
const unsubscribeNodePanelPresence = collaborationManager.onNodePanelPresenceUpdate((presence) => {
|
||||
setState((prev: any) => ({ ...prev, nodePanelPresence: presence }))
|
||||
const unsubscribeNodePanelPresence = collaborationManager.onNodePanelPresenceUpdate((presence: NodePanelPresenceMap) => {
|
||||
setState(prev => ({ ...prev, nodePanelPresence: presence }))
|
||||
})
|
||||
|
||||
const unsubscribeLeaderChange = collaborationManager.onLeaderChange((isLeader: boolean) => {
|
||||
console.log('Leader status changed:', isLeader)
|
||||
setState((prev: any) => ({ ...prev, isLeader }))
|
||||
setState(prev => ({ ...prev, isLeader }))
|
||||
})
|
||||
|
||||
return () => {
|
||||
|
||||
@ -1,38 +1,34 @@
|
||||
export type CollaborationEvent = {
|
||||
export type CollaborationEvent<TData = unknown> = {
|
||||
type: string
|
||||
data: any
|
||||
data: TData
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export type GraphUpdateEvent = {
|
||||
type: 'graph_update'
|
||||
data: Uint8Array
|
||||
} & CollaborationEvent
|
||||
} & CollaborationEvent<Uint8Array>
|
||||
|
||||
export type CursorMoveEvent = {
|
||||
type: 'cursor_move'
|
||||
data: {
|
||||
x: number
|
||||
y: number
|
||||
userId: string
|
||||
}
|
||||
} & CollaborationEvent
|
||||
} & CollaborationEvent<{
|
||||
x: number
|
||||
y: number
|
||||
userId: string
|
||||
}>
|
||||
|
||||
export type UserConnectEvent = {
|
||||
type: 'user_connect'
|
||||
data: {
|
||||
workflow_id: string
|
||||
}
|
||||
} & CollaborationEvent
|
||||
} & CollaborationEvent<{
|
||||
workflow_id: string
|
||||
}>
|
||||
|
||||
export type OnlineUsersEvent = {
|
||||
type: 'online_users'
|
||||
data: {
|
||||
users: Array<{
|
||||
user_id: string
|
||||
username: string
|
||||
avatar: string
|
||||
sid: string
|
||||
}>
|
||||
}
|
||||
} & CollaborationEvent
|
||||
} & CollaborationEvent<{
|
||||
users: Array<{
|
||||
user_id: string
|
||||
username: string
|
||||
avatar: string
|
||||
sid: string
|
||||
}>
|
||||
}>
|
||||
|
||||
@ -253,11 +253,15 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
|
||||
}, [value, syncHighlightScroll])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
evaluateContentLayout()
|
||||
Promise.resolve().then(() => {
|
||||
evaluateContentLayout()
|
||||
})
|
||||
}, [value, evaluateContentLayout])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
updateLayoutPadding()
|
||||
Promise.resolve().then(() => {
|
||||
updateLayoutPadding()
|
||||
})
|
||||
}, [updateLayoutPadding, isEditing, shouldReserveButtonGap])
|
||||
|
||||
useEffect(() => {
|
||||
@ -271,9 +275,11 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
|
||||
}, [evaluateContentLayout, updateLayoutPadding])
|
||||
|
||||
useEffect(() => {
|
||||
baseTextareaHeightRef.current = null
|
||||
evaluateContentLayout()
|
||||
setShouldReserveHorizontalSpace(!isEditing)
|
||||
Promise.resolve().then(() => {
|
||||
baseTextareaHeightRef.current = null
|
||||
evaluateContentLayout()
|
||||
setShouldReserveHorizontalSpace(!isEditing)
|
||||
})
|
||||
}, [isEditing, evaluateContentLayout])
|
||||
|
||||
const filteredMentionUsers = useMemo(() => {
|
||||
@ -481,8 +487,11 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!value)
|
||||
resetMentionState()
|
||||
if (!value) {
|
||||
Promise.resolve().then(() => {
|
||||
resetMentionState()
|
||||
})
|
||||
}
|
||||
}, [value, resetMentionState])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -190,7 +190,9 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
}, [mentionUsers])
|
||||
|
||||
useEffect(() => {
|
||||
setReplyContent('')
|
||||
Promise.resolve().then(() => {
|
||||
setReplyContent('')
|
||||
})
|
||||
}, [comment.id])
|
||||
|
||||
useEffect(() => () => {
|
||||
|
||||
@ -62,8 +62,6 @@ const Features = () => {
|
||||
file_upload: currentFeatures.file,
|
||||
}
|
||||
|
||||
console.log('Sending features to server:', transformedFeatures)
|
||||
|
||||
await updateFeatures({
|
||||
appId,
|
||||
features: transformedFeatures,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
'use client'
|
||||
import type { OnlineUser } from '../collaboration/types'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
@ -16,7 +17,7 @@ import { useCollaboration } from '../collaboration/hooks/use-collaboration'
|
||||
import { getUserColor } from '../collaboration/utils/user-color'
|
||||
import { useStore } from '../store'
|
||||
|
||||
const useAvatarUrls = (users: any[]) => {
|
||||
const useAvatarUrls = (users: OnlineUser[]) => {
|
||||
const [avatarUrls, setAvatarUrls] = useState<Record<string, string>>({})
|
||||
|
||||
useEffect(() => {
|
||||
@ -59,7 +60,7 @@ const OnlineUsers = () => {
|
||||
const currentUserId = userProfile?.id
|
||||
|
||||
const renderDisplayName = (
|
||||
user: any,
|
||||
user: OnlineUser,
|
||||
baseClassName: string,
|
||||
suffixClassName: string,
|
||||
) => {
|
||||
@ -99,7 +100,7 @@ const OnlineUsers = () => {
|
||||
const visibleUsers = onlineUsers.slice(0, maxVisible)
|
||||
const remainingCount = onlineUsers.length - maxVisible
|
||||
|
||||
const getAvatarUrl = (user: any) => {
|
||||
const getAvatarUrl = (user: OnlineUser) => {
|
||||
return avatarUrls[user.sid] || user.avatar
|
||||
}
|
||||
|
||||
|
||||
@ -27,7 +27,9 @@ const UndoRedo: FC<UndoRedoProps> = ({ handleUndo, handleRedo }) => {
|
||||
}
|
||||
|
||||
// Initial state
|
||||
updateButtonStates()
|
||||
Promise.resolve().then(() => {
|
||||
updateButtonStates()
|
||||
})
|
||||
|
||||
// Listen for undo/redo state changes
|
||||
const unsubscribe = collaborationManager.onUndoRedoStateChange((state) => {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import type { FileUpload } from '../../base/features/types'
|
||||
import type { TriggerType } from '@/app/components/workflow/header/test-run-menu'
|
||||
import type {
|
||||
BlockEnum,
|
||||
CommonNodeType,
|
||||
Node,
|
||||
NodeDefault,
|
||||
ToolWithProvider,
|
||||
@ -9,7 +11,7 @@ import type {
|
||||
import type { IOtherOptions } from '@/service/base'
|
||||
import type { SchemaTypeDefinition } from '@/service/use-common'
|
||||
import type { FlowType } from '@/types/common'
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import type { FetchWorkflowDraftResponse, VarInInspect } from '@/types/workflow'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useContext } from 'react'
|
||||
import {
|
||||
@ -18,9 +20,17 @@ import {
|
||||
import { createStore } from 'zustand/vanilla'
|
||||
import { HooksStoreContext } from './provider'
|
||||
|
||||
export type AvailableNodeDefault = NodeDefault<CommonNodeType<Record<string, unknown>>>
|
||||
export type WorkflowRunOptions = {
|
||||
mode?: TriggerType
|
||||
scheduleNodeId?: string
|
||||
webhookNodeId?: string
|
||||
pluginNodeId?: string
|
||||
allNodeIds?: string[]
|
||||
}
|
||||
export type AvailableNodesMetaData = {
|
||||
nodes: NodeDefault[]
|
||||
nodesMap?: Record<BlockEnum, NodeDefault<any>>
|
||||
nodes: AvailableNodeDefault[]
|
||||
nodesMap?: Partial<Record<BlockEnum, AvailableNodeDefault>>
|
||||
}
|
||||
export type CommonHooksFnMap = {
|
||||
doSyncWorkflowDraft: (
|
||||
@ -36,9 +46,9 @@ export type CommonHooksFnMap = {
|
||||
handleRefreshWorkflowDraft: () => void
|
||||
handleBackupDraft: () => void
|
||||
handleLoadBackupDraft: () => void
|
||||
handleRestoreFromPublishedWorkflow: (...args: any[]) => void
|
||||
handleRun: (params: any, callback?: IOtherOptions, options?: any) => void
|
||||
handleStopRun: (...args: any[]) => void
|
||||
handleRestoreFromPublishedWorkflow: (publishedWorkflow: FetchWorkflowDraftResponse) => void
|
||||
handleRun: (params: unknown, callback?: IOtherOptions, options?: WorkflowRunOptions) => void | Promise<void>
|
||||
handleStopRun: (taskId: string) => void
|
||||
handleStartWorkflowRun: () => void
|
||||
handleWorkflowStartRunInWorkflow: () => void
|
||||
handleWorkflowStartRunInChatflow: () => void
|
||||
@ -54,7 +64,7 @@ export type CommonHooksFnMap = {
|
||||
hasNodeInspectVars: (nodeId: string) => boolean
|
||||
hasSetInspectVar: (nodeId: string, name: string, sysVars: VarInInspect[], conversationVars: VarInInspect[]) => boolean
|
||||
fetchInspectVarValue: (selector: ValueSelector, schemaTypeDefinitions: SchemaTypeDefinition[]) => Promise<void>
|
||||
editInspectVarValue: (nodeId: string, varId: string, value: any) => Promise<void>
|
||||
editInspectVarValue: (nodeId: string, varId: string, value: unknown) => Promise<void>
|
||||
renameInspectVarName: (nodeId: string, oldName: string, newName: string) => Promise<void>
|
||||
appendNodeInspectVars: (nodeId: string, payload: VarInInspect[], allNodes: Node[]) => void
|
||||
deleteInspectVar: (nodeId: string, varId: string) => Promise<void>
|
||||
@ -68,7 +78,7 @@ export type CommonHooksFnMap = {
|
||||
configsMap?: {
|
||||
flowId: string
|
||||
flowType: FlowType
|
||||
fileSettings: FileUpload
|
||||
fileSettings?: FileUpload
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,9 +92,9 @@ export const createHooksStore = ({
|
||||
handleRefreshWorkflowDraft = noop,
|
||||
handleBackupDraft = noop,
|
||||
handleLoadBackupDraft = noop,
|
||||
handleRestoreFromPublishedWorkflow = noop,
|
||||
handleRestoreFromPublishedWorkflow = (_publishedWorkflow: FetchWorkflowDraftResponse) => noop(),
|
||||
handleRun = noop,
|
||||
handleStopRun = noop,
|
||||
handleStopRun = (_taskId: string) => noop(),
|
||||
handleStartWorkflowRun = noop,
|
||||
handleWorkflowStartRunInWorkflow = noop,
|
||||
handleWorkflowStartRunInChatflow = noop,
|
||||
|
||||
@ -362,7 +362,10 @@ export const useChecklistBeforePublish = () => {
|
||||
usedVars = getNodeUsedVars(node).filter(v => v.length > 0)
|
||||
}
|
||||
const checkData = getCheckData(node.data, datasets)
|
||||
const { errorMessage } = nodesExtraData![node.data.type as BlockEnum].checkValid(checkData, t, moreDataForCheckValid)
|
||||
const nodeMetaData = nodesExtraData?.[node.data.type as BlockEnum]
|
||||
if (!nodeMetaData)
|
||||
continue
|
||||
const { errorMessage } = nodeMetaData.checkValid(checkData, t, moreDataForCheckValid)
|
||||
|
||||
if (errorMessage) {
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${errorMessage}` })
|
||||
|
||||
@ -1,18 +1,16 @@
|
||||
import type { SyncCallback } from './use-nodes-sync-draft'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useCollaborativeWorkflow } from './use-collaborative-workflow'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { useNodesReadOnly } from './use-workflow'
|
||||
|
||||
type NodeDataUpdatePayload = {
|
||||
id: string
|
||||
data: Record<string, any>
|
||||
data: Record<string, unknown>
|
||||
}
|
||||
|
||||
export const useNodeDataUpdate = () => {
|
||||
const store = useStoreApi()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
@ -26,7 +24,7 @@ export const useNodeDataUpdate = () => {
|
||||
currentNode.data = { ...currentNode.data, ...data }
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const handleNodeDataUpdateWithSyncDraft = useCallback((
|
||||
payload: NodeDataUpdatePayload,
|
||||
|
||||
@ -815,7 +815,10 @@ export const useNodesInteractions = () => {
|
||||
const nodesWithSameType = nodes.filter(
|
||||
node => node.data.type === nodeType,
|
||||
)
|
||||
const { defaultValue } = nodesMetaDataMap![nodeType]
|
||||
const nodeMetaData = nodesMetaDataMap?.[nodeType]
|
||||
if (!nodeMetaData)
|
||||
return
|
||||
const { defaultValue } = nodeMetaData
|
||||
const { newNode, newIterationStartNode, newLoopStartNode }
|
||||
= generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(nodeType),
|
||||
@ -1376,7 +1379,10 @@ export const useNodesInteractions = () => {
|
||||
const nodesWithSameType = nodes.filter(
|
||||
node => node.data.type === nodeType,
|
||||
)
|
||||
const { defaultValue } = nodesMetaDataMap![nodeType]
|
||||
const nodeMetaData = nodesMetaDataMap?.[nodeType]
|
||||
if (!nodeMetaData)
|
||||
return
|
||||
const { defaultValue } = nodeMetaData
|
||||
const {
|
||||
newNode: newCurrentNode,
|
||||
newIterationStartNode,
|
||||
@ -1537,7 +1543,9 @@ export const useNodesInteractions = () => {
|
||||
return false
|
||||
if (node.type === CUSTOM_NOTE_NODE)
|
||||
return true
|
||||
const { metaData } = nodesMetaDataMap![node.data.type as BlockEnum]
|
||||
const metaData = nodesMetaDataMap?.[node.data.type as BlockEnum]?.metaData
|
||||
if (!metaData)
|
||||
return false
|
||||
if (metaData.isSingleton)
|
||||
return false
|
||||
return !node.data.isInIteration && !node.data.isInLoop
|
||||
@ -1553,7 +1561,9 @@ export const useNodesInteractions = () => {
|
||||
return false
|
||||
if (node.type === CUSTOM_NOTE_NODE)
|
||||
return true
|
||||
const { metaData } = nodesMetaDataMap![node.data.type as BlockEnum]
|
||||
const metaData = nodesMetaDataMap?.[node.data.type as BlockEnum]?.metaData
|
||||
if (!metaData)
|
||||
return false
|
||||
return !metaData.isSingleton
|
||||
})
|
||||
|
||||
@ -1588,12 +1598,15 @@ export const useNodesInteractions = () => {
|
||||
const parentChildrenToAppend: { parentId: string, childId: string, childType: BlockEnum }[] = []
|
||||
clipboardElements.forEach((nodeToPaste, index) => {
|
||||
const nodeType = nodeToPaste.data.type
|
||||
const nodeDefaultValue = nodeToPaste.type !== CUSTOM_NOTE_NODE
|
||||
? nodesMetaDataMap?.[nodeType]?.defaultValue
|
||||
: undefined
|
||||
|
||||
const { newNode, newIterationStartNode, newLoopStartNode }
|
||||
= generateNewNode({
|
||||
type: nodeToPaste.type,
|
||||
data: {
|
||||
...(nodeToPaste.type !== CUSTOM_NOTE_NODE && nodesMetaDataMap![nodeType].defaultValue),
|
||||
...(nodeDefaultValue || {}),
|
||||
...nodeToPaste.data,
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
@ -1898,16 +1911,7 @@ export const useNodesInteractions = () => {
|
||||
return
|
||||
|
||||
// Use collaborative undo from Loro
|
||||
const undoResult = collaborationManager.undo()
|
||||
|
||||
if (undoResult) {
|
||||
// The undo operation will automatically trigger subscriptions
|
||||
// which will update the nodes and edges through setupSubscriptions
|
||||
console.log('Collaborative undo performed')
|
||||
}
|
||||
else {
|
||||
console.log('Nothing to undo')
|
||||
}
|
||||
collaborationManager.undo()
|
||||
const { edges, nodes } = workflowHistoryStore.getState()
|
||||
if (edges.length === 0 && nodes.length === 0)
|
||||
return
|
||||
@ -1928,16 +1932,7 @@ export const useNodesInteractions = () => {
|
||||
return
|
||||
|
||||
// Use collaborative redo from Loro
|
||||
const redoResult = collaborationManager.redo()
|
||||
|
||||
if (redoResult) {
|
||||
// The redo operation will automatically trigger subscriptions
|
||||
// which will update the nodes and edges through setupSubscriptions
|
||||
console.log('Collaborative redo performed')
|
||||
}
|
||||
else {
|
||||
console.log('Nothing to redo')
|
||||
}
|
||||
collaborationManager.redo()
|
||||
const { edges, nodes } = workflowHistoryStore.getState()
|
||||
if (edges.length === 0 && nodes.length === 0)
|
||||
return
|
||||
|
||||
@ -10,6 +10,13 @@ import { useStore } from '../store'
|
||||
import { ControlMode } from '../types'
|
||||
|
||||
const EMPTY_USERS: UserProfile[] = []
|
||||
type CommentDetailResponse = WorkflowCommentDetail | { data: WorkflowCommentDetail }
|
||||
|
||||
const getCommentDetail = (response: CommentDetailResponse): WorkflowCommentDetail => {
|
||||
if ('data' in response)
|
||||
return response.data
|
||||
return response
|
||||
}
|
||||
|
||||
export const useWorkflowComment = () => {
|
||||
const params = useParams()
|
||||
@ -56,8 +63,8 @@ export const useWorkflowComment = () => {
|
||||
if (!appId)
|
||||
return
|
||||
|
||||
const detailResponse = await fetchWorkflowComment(appId, commentId)
|
||||
const detail = (detailResponse as any)?.data ?? detailResponse
|
||||
const detailResponse = await fetchWorkflowComment(appId, commentId) as CommentDetailResponse
|
||||
const detail = getCommentDetail(detailResponse)
|
||||
|
||||
commentDetailCacheRef.current = {
|
||||
...commentDetailCacheRef.current,
|
||||
@ -106,8 +113,6 @@ export const useWorkflowComment = () => {
|
||||
if (!pendingComment)
|
||||
return
|
||||
|
||||
console.log('Submitting comment:', { appId, pendingComment, content, mentionedUserIds })
|
||||
|
||||
if (!appId) {
|
||||
console.error('AppId is missing')
|
||||
return
|
||||
@ -128,9 +133,10 @@ export const useWorkflowComment = () => {
|
||||
mentioned_user_ids: mentionedUserIds,
|
||||
})
|
||||
|
||||
console.log('Comment created successfully:', newComment)
|
||||
|
||||
const createdAt = (newComment as any)?.created_at
|
||||
const createdAt = Number(newComment.created_at)
|
||||
const createdAtSeconds = Number.isNaN(createdAt)
|
||||
? Math.floor(Date.parse(newComment.created_at) / 1000)
|
||||
: createdAt
|
||||
const createdByAccount = {
|
||||
id: userProfile?.id ?? '',
|
||||
name: userProfile?.name ?? '',
|
||||
@ -162,8 +168,8 @@ export const useWorkflowComment = () => {
|
||||
content,
|
||||
created_by: createdByAccount.id,
|
||||
created_by_account: createdByAccount,
|
||||
created_at: createdAt,
|
||||
updated_at: createdAt,
|
||||
created_at: createdAtSeconds,
|
||||
updated_at: createdAtSeconds,
|
||||
resolved: false,
|
||||
mention_count: mentionedUserIds.length,
|
||||
reply_count: 0,
|
||||
@ -177,8 +183,8 @@ export const useWorkflowComment = () => {
|
||||
content,
|
||||
created_by: createdByAccount.id,
|
||||
created_by_account: createdByAccount,
|
||||
created_at: createdAt,
|
||||
updated_at: createdAt,
|
||||
created_at: createdAtSeconds,
|
||||
updated_at: createdAtSeconds,
|
||||
resolved: false,
|
||||
replies: [],
|
||||
mentions: mentionedUserIds.map(mentionedId => ({
|
||||
@ -246,8 +252,8 @@ export const useWorkflowComment = () => {
|
||||
setActiveCommentLoading(!cachedDetail)
|
||||
|
||||
try {
|
||||
const detailResponse = await fetchWorkflowComment(appId, comment.id)
|
||||
const detail = (detailResponse as any)?.data ?? detailResponse
|
||||
const detailResponse = await fetchWorkflowComment(appId, comment.id) as CommentDetailResponse
|
||||
const detail = getCommentDetail(detailResponse)
|
||||
|
||||
commentDetailCacheRef.current = {
|
||||
...commentDetailCacheRef.current,
|
||||
@ -499,13 +505,8 @@ export const useWorkflowComment = () => {
|
||||
elementX: number
|
||||
elementY: number
|
||||
}) => {
|
||||
if (controlMode === ControlMode.Comment) {
|
||||
console.log('Setting pending comment at screen position:', mousePosition)
|
||||
if (controlMode === ControlMode.Comment)
|
||||
setPendingComment(mousePosition)
|
||||
}
|
||||
else {
|
||||
console.log('Control mode is not Comment:', controlMode)
|
||||
}
|
||||
}, [controlMode, setPendingComment])
|
||||
|
||||
return {
|
||||
|
||||
@ -4,10 +4,13 @@ import type { FC } from 'react'
|
||||
import type {
|
||||
Viewport,
|
||||
} from 'reactflow'
|
||||
import type { CursorPosition, OnlineUser } from './collaboration/types'
|
||||
import type { Shape as HooksStoreShape } from './hooks-store'
|
||||
import type { WorkflowSliceShape } from './store/workflow/workflow-slice'
|
||||
import type {
|
||||
ConversationVariable,
|
||||
Edge,
|
||||
EnvironmentVariable,
|
||||
Node,
|
||||
} from './types'
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
@ -124,15 +127,37 @@ const edgeTypes = {
|
||||
[CUSTOM_EDGE]: CustomEdge,
|
||||
}
|
||||
|
||||
type WorkflowDataUpdatePayload = {
|
||||
nodes: Node[]
|
||||
edges: Edge[]
|
||||
viewport?: Viewport
|
||||
hash?: string
|
||||
features?: unknown
|
||||
conversation_variables?: ConversationVariable[]
|
||||
environment_variables?: EnvironmentVariable[]
|
||||
}
|
||||
|
||||
type WorkflowEvent = {
|
||||
type?: string
|
||||
payload?: unknown
|
||||
}
|
||||
|
||||
const isWorkflowDataUpdatePayload = (payload: unknown): payload is WorkflowDataUpdatePayload => {
|
||||
if (!payload || typeof payload !== 'object')
|
||||
return false
|
||||
const candidate = payload as WorkflowDataUpdatePayload
|
||||
return Array.isArray(candidate.nodes) && Array.isArray(candidate.edges)
|
||||
}
|
||||
|
||||
export type WorkflowProps = {
|
||||
nodes: Node[]
|
||||
edges: Edge[]
|
||||
viewport?: Viewport
|
||||
children?: React.ReactNode
|
||||
onWorkflowDataUpdate?: (v: any) => void
|
||||
cursors?: Record<string, any>
|
||||
onWorkflowDataUpdate?: (v: WorkflowDataUpdatePayload) => void
|
||||
cursors?: Record<string, CursorPosition>
|
||||
myUserId?: string | null
|
||||
onlineUsers?: any[]
|
||||
onlineUsers?: OnlineUser[]
|
||||
}
|
||||
export const Workflow: FC<WorkflowProps> = memo(({
|
||||
nodes: originalNodes,
|
||||
@ -236,19 +261,20 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
const { t } = useTranslation()
|
||||
|
||||
const store = useStoreApi()
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === WORKFLOW_DATA_UPDATE) {
|
||||
setNodes(v.payload.nodes)
|
||||
store.getState().setNodes(v.payload.nodes)
|
||||
setEdges(v.payload.edges)
|
||||
eventEmitter?.useSubscription((event) => {
|
||||
const workflowEvent = event as unknown as WorkflowEvent
|
||||
if (workflowEvent.type === WORKFLOW_DATA_UPDATE && isWorkflowDataUpdatePayload(workflowEvent.payload)) {
|
||||
setNodes(workflowEvent.payload.nodes)
|
||||
store.getState().setNodes(workflowEvent.payload.nodes)
|
||||
setEdges(workflowEvent.payload.edges)
|
||||
|
||||
if (v.payload.viewport)
|
||||
reactflow.setViewport(v.payload.viewport)
|
||||
if (workflowEvent.payload.viewport)
|
||||
reactflow.setViewport(workflowEvent.payload.viewport)
|
||||
|
||||
if (v.payload.hash)
|
||||
setSyncWorkflowDraftHash(v.payload.hash)
|
||||
if (workflowEvent.payload.hash)
|
||||
setSyncWorkflowDraftHash(workflowEvent.payload.hash)
|
||||
|
||||
onWorkflowDataUpdate?.(v.payload)
|
||||
onWorkflowDataUpdate?.(workflowEvent.payload)
|
||||
|
||||
setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
|
||||
}
|
||||
@ -635,9 +661,9 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
|
||||
type WorkflowWithInnerContextProps = WorkflowProps & {
|
||||
hooksStore?: Partial<HooksStoreShape>
|
||||
cursors?: Record<string, any>
|
||||
cursors?: Record<string, CursorPosition>
|
||||
myUserId?: string | null
|
||||
onlineUsers?: any[]
|
||||
onlineUsers?: OnlineUser[]
|
||||
}
|
||||
export const WorkflowWithInnerContext = memo(({
|
||||
hooksStore,
|
||||
|
||||
@ -42,7 +42,9 @@ export const TitleInput = memo(({
|
||||
|
||||
// Sync local state with incoming collaborative updates so remote title edits appear immediately.
|
||||
useEffect(() => {
|
||||
setLocalValue(value)
|
||||
Promise.resolve().then(() => {
|
||||
setLocalValue(value)
|
||||
})
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
|
||||
@ -22,9 +22,10 @@ export const useReplaceDataSourceNode = (id: string) => {
|
||||
|
||||
if (emptyNodeIndex < 0)
|
||||
return
|
||||
const {
|
||||
defaultValue,
|
||||
} = nodesMetaDataMap![type]
|
||||
const nodeMetaData = nodesMetaDataMap?.[type]
|
||||
if (!nodeMetaData)
|
||||
return
|
||||
const { defaultValue } = nodeMetaData
|
||||
const emptyNode = nodes[emptyNodeIndex]
|
||||
const { newNode } = generateNewNode({
|
||||
data: {
|
||||
|
||||
@ -41,13 +41,15 @@ const useKeyValueList = (value: string, onChange: (value: string) => void, noFil
|
||||
}, [noFilter, onChange, value])
|
||||
|
||||
useEffect(() => {
|
||||
doSetList((prev) => {
|
||||
const targetItems = value ? strToKeyValueList(value) : []
|
||||
const currentValue = stringifyList(prev, noFilter)
|
||||
const targetValue = stringifyList(targetItems, noFilter)
|
||||
if (currentValue === targetValue)
|
||||
return prev
|
||||
return normalizeList(targetItems)
|
||||
Promise.resolve().then(() => {
|
||||
doSetList((prev) => {
|
||||
const targetItems = value ? strToKeyValueList(value) : []
|
||||
const currentValue = stringifyList(prev, noFilter)
|
||||
const targetValue = stringifyList(targetItems, noFilter)
|
||||
if (currentValue === targetValue)
|
||||
return prev
|
||||
return normalizeList(targetItems)
|
||||
})
|
||||
})
|
||||
}, [value, noFilter])
|
||||
const addItem = useCallback(() => {
|
||||
|
||||
@ -115,6 +115,7 @@ export const useNodeIterationInteractions = () => {
|
||||
const copyChildren = childrenNodes.map((child, index) => {
|
||||
const childNodeType = child.data.type as BlockEnum
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
|
||||
const defaultValue = nodesMetaDataMap?.[childNodeType]?.defaultValue ?? {}
|
||||
|
||||
if (!childNodeTypeCount[childNodeType])
|
||||
childNodeTypeCount[childNodeType] = nodesWithSameType.length + 1
|
||||
@ -124,7 +125,7 @@ export const useNodeIterationInteractions = () => {
|
||||
const { newNode } = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(childNodeType),
|
||||
data: {
|
||||
...nodesMetaDataMap![childNodeType].defaultValue,
|
||||
...defaultValue,
|
||||
...child.data,
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
|
||||
@ -109,7 +109,7 @@ export const useNodeLoopInteractions = () => {
|
||||
|
||||
return childrenNodes.map((child, index) => {
|
||||
const childNodeType = child.data.type as BlockEnum
|
||||
const { defaultValue } = nodesMetaDataMap![childNodeType]
|
||||
const defaultValue = nodesMetaDataMap?.[childNodeType]?.defaultValue ?? {}
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
|
||||
const { newNode } = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(childNodeType),
|
||||
|
||||
@ -63,9 +63,10 @@ const AddBlock = ({
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === type)
|
||||
const {
|
||||
defaultValue,
|
||||
} = nodesMetaDataMap![type]
|
||||
const nodeMetaData = nodesMetaDataMap?.[type]
|
||||
if (!nodeMetaData)
|
||||
return
|
||||
const { defaultValue } = nodeMetaData
|
||||
const { newNode } = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(type),
|
||||
data: {
|
||||
|
||||
Reference in New Issue
Block a user