import type { UserProfile, WorkflowCommentDetail, WorkflowCommentList } from '@/service/workflow-comment' import { useParams } from 'next/navigation' import { useCallback, useEffect, useRef } from 'react' import { useReactFlow } from 'reactflow' import { collaborationManager } from '@/app/components/workflow/collaboration' import { useAppContext } from '@/context/app-context' import { useGlobalPublicStore } from '@/context/global-public-context' import { createWorkflowComment, createWorkflowCommentReply, deleteWorkflowComment, deleteWorkflowCommentReply, fetchWorkflowComment, fetchWorkflowComments, resolveWorkflowComment, updateWorkflowComment, updateWorkflowCommentReply } from '@/service/workflow-comment' import { useStore } from '../store' import { ControlMode } from '../types' const EMPTY_USERS: UserProfile[] = [] export const useWorkflowComment = () => { const params = useParams() const appId = params.appId as string const reactflow = useReactFlow() const controlMode = useStore(s => s.controlMode) const pendingComment = useStore(s => s.pendingComment) const setPendingComment = useStore(s => s.setPendingComment) const setActiveCommentId = useStore(s => s.setActiveCommentId) const activeCommentId = useStore(s => s.activeCommentId) const comments = useStore(s => s.comments) const setComments = useStore(s => s.setComments) const loading = useStore(s => s.commentsLoading) const setCommentsLoading = useStore(s => s.setCommentsLoading) const activeComment = useStore(s => s.activeCommentDetail) const setActiveComment = useStore(s => s.setActiveCommentDetail) const activeCommentLoading = useStore(s => s.activeCommentDetailLoading) const setActiveCommentLoading = useStore(s => s.setActiveCommentDetailLoading) const replySubmitting = useStore(s => s.replySubmitting) const setReplySubmitting = useStore(s => s.setReplySubmitting) const replyUpdating = useStore(s => s.replyUpdating) const setReplyUpdating = useStore(s => s.setReplyUpdating) const commentDetailCache = useStore(s => s.commentDetailCache) const setCommentDetailCache = useStore(s => s.setCommentDetailCache) const rightPanelWidth = useStore(s => s.rightPanelWidth) const nodePanelWidth = useStore(s => s.nodePanelWidth) const mentionableUsers = useStore(state => ( appId ? state.mentionableUsersCache[appId] ?? EMPTY_USERS : EMPTY_USERS )) const { userProfile } = useAppContext() const isCollaborationEnabled = useGlobalPublicStore(s => s.systemFeatures.enable_collaboration_mode) const commentDetailCacheRef = useRef>(commentDetailCache) const activeCommentIdRef = useRef(null) useEffect(() => { activeCommentIdRef.current = activeCommentId ?? null }, [activeCommentId]) useEffect(() => { commentDetailCacheRef.current = commentDetailCache }, [commentDetailCache]) const refreshActiveComment = useCallback(async (commentId: string) => { if (!appId) return const detailResponse = await fetchWorkflowComment(appId, commentId) const detail = (detailResponse as any)?.data ?? detailResponse commentDetailCacheRef.current = { ...commentDetailCacheRef.current, [commentId]: detail, } setCommentDetailCache(commentDetailCacheRef.current) setActiveComment(detail) }, [appId, setActiveComment, setCommentDetailCache]) const loadComments = useCallback(async () => { if (!appId || !isCollaborationEnabled) return setCommentsLoading(true) try { const commentsData = await fetchWorkflowComments(appId) setComments(commentsData) } catch (error) { console.error('Failed to fetch comments:', error) } finally { setCommentsLoading(false) } }, [appId, isCollaborationEnabled, setComments, setCommentsLoading]) // Setup collaboration useEffect(() => { if (!appId || !isCollaborationEnabled) return const unsubscribe = collaborationManager.onCommentsUpdate(() => { loadComments() if (activeCommentIdRef.current) refreshActiveComment(activeCommentIdRef.current) }) return unsubscribe }, [appId, isCollaborationEnabled, loadComments, refreshActiveComment]) useEffect(() => { loadComments() }, [loadComments]) const handleCommentSubmit = useCallback(async (content: string, mentionedUserIds: string[] = []) => { if (!pendingComment) return console.log('Submitting comment:', { appId, pendingComment, content, mentionedUserIds }) if (!appId) { console.error('AppId is missing') return } try { // Convert screen position to flow position when submitting const { screenToFlowPosition } = reactflow const flowPosition = screenToFlowPosition({ x: pendingComment.pageX, y: pendingComment.pageY, }) const newComment = await createWorkflowComment(appId, { position_x: flowPosition.x, position_y: flowPosition.y, content, mentioned_user_ids: mentionedUserIds, }) console.log('Comment created successfully:', newComment) const createdAt = (newComment as any)?.created_at const createdByAccount = { id: userProfile?.id ?? '', name: userProfile?.name ?? '', email: userProfile?.email ?? '', avatar_url: userProfile?.avatar_url || userProfile?.avatar || undefined, } const mentionedUsers = mentionedUserIds .map(mentionedId => mentionableUsers.find(user => user.id === mentionedId)) .filter((user): user is NonNullable => Boolean(user)) const uniqueParticipantsMap = new Map() if (createdByAccount.id) uniqueParticipantsMap.set(createdByAccount.id, createdByAccount) for (const user of mentionedUsers) { if (!uniqueParticipantsMap.has(user.id)) { uniqueParticipantsMap.set(user.id, { id: user.id, name: user.name, email: user.email, avatar_url: user.avatar_url, }) } } const participants = Array.from(uniqueParticipantsMap.values()) const composedComment: WorkflowCommentList = { id: newComment.id, position_x: flowPosition.x, position_y: flowPosition.y, content, created_by: createdByAccount.id, created_by_account: createdByAccount, created_at: createdAt, updated_at: createdAt, resolved: false, mention_count: mentionedUserIds.length, reply_count: 0, participants, } const composedDetail: WorkflowCommentDetail = { id: newComment.id, position_x: flowPosition.x, position_y: flowPosition.y, content, created_by: createdByAccount.id, created_by_account: createdByAccount, created_at: createdAt, updated_at: createdAt, resolved: false, replies: [], mentions: mentionedUserIds.map(mentionedId => ({ mentioned_user_id: mentionedId, mentioned_user_account: mentionableUsers.find(user => user.id === mentionedId) ?? null, reply_id: null, })), } setComments([...comments, composedComment]) commentDetailCacheRef.current = { ...commentDetailCacheRef.current, [newComment.id]: composedDetail, } setCommentDetailCache(commentDetailCacheRef.current) collaborationManager.emitCommentsUpdate(appId) setPendingComment(null) } catch (error) { console.error('Failed to create comment:', error) setPendingComment(null) } }, [appId, pendingComment, setPendingComment, reactflow, comments, setComments, userProfile, setCommentDetailCache, mentionableUsers]) const handleCommentCancel = useCallback(() => { setPendingComment(null) }, [setPendingComment]) useEffect(() => { if (controlMode !== ControlMode.Comment) setPendingComment(null) }, [controlMode, setPendingComment]) const handleCommentIconClick = useCallback(async (comment: WorkflowCommentList) => { setPendingComment(null) activeCommentIdRef.current = comment.id setActiveCommentId(comment.id) const cachedDetail = commentDetailCacheRef.current[comment.id] setActiveComment(cachedDetail || comment) const hasSelectedNode = reactflow.getNodes().some(node => node.data?.selected) const commentPanelWidth = controlMode === ControlMode.Comment ? 420 : 0 const fallbackPanelWidth = (hasSelectedNode ? nodePanelWidth : 0) + commentPanelWidth const effectivePanelWidth = Math.max(rightPanelWidth ?? 0, fallbackPanelWidth) const baseHorizontalOffsetPx = 220 const panelCompensationPx = effectivePanelWidth / 2 const desiredHorizontalOffsetPx = baseHorizontalOffsetPx + panelCompensationPx const maxOffset = Math.max(0, (window.innerWidth / 2) - 60) const horizontalOffsetPx = Math.min(desiredHorizontalOffsetPx, maxOffset) reactflow.setCenter( comment.position_x + horizontalOffsetPx, comment.position_y, { zoom: 1, duration: 600 }, ) if (!appId) return setActiveCommentLoading(!cachedDetail) try { const detailResponse = await fetchWorkflowComment(appId, comment.id) const detail = (detailResponse as any)?.data ?? detailResponse commentDetailCacheRef.current = { ...commentDetailCacheRef.current, [comment.id]: detail, } setCommentDetailCache(commentDetailCacheRef.current) if (activeCommentIdRef.current === comment.id) setActiveComment(detail) } catch (e) { console.warn('Failed to load workflow comment detail', e) } finally { setActiveCommentLoading(false) } }, [ appId, controlMode, nodePanelWidth, reactflow, rightPanelWidth, setActiveComment, setActiveCommentId, setActiveCommentLoading, setCommentDetailCache, setPendingComment, ]) const handleCommentResolve = useCallback(async (commentId: string) => { if (!appId) return setActiveCommentLoading(true) try { await resolveWorkflowComment(appId, commentId) collaborationManager.emitCommentsUpdate(appId) await refreshActiveComment(commentId) await loadComments() } catch (error) { console.error('Failed to resolve comment:', error) } finally { setActiveCommentLoading(false) } }, [appId, loadComments, refreshActiveComment, setActiveCommentLoading]) const handleCommentDelete = useCallback(async (commentId: string) => { if (!appId) return setActiveCommentLoading(true) try { await deleteWorkflowComment(appId, commentId) collaborationManager.emitCommentsUpdate(appId) const updatedCache = { ...commentDetailCacheRef.current } delete updatedCache[commentId] commentDetailCacheRef.current = updatedCache setCommentDetailCache(updatedCache) const currentComments = comments.filter(c => c.id !== commentId) const commentIndex = comments.findIndex(c => c.id === commentId) const fallbackTarget = commentIndex >= 0 ? comments[commentIndex + 1] ?? comments[commentIndex - 1] : undefined await loadComments() if (fallbackTarget) { handleCommentIconClick(fallbackTarget) } else if (currentComments.length > 0) { const nextComment = currentComments[0] handleCommentIconClick(nextComment) } else { setActiveComment(null) setActiveCommentId(null) activeCommentIdRef.current = null } } catch (error) { console.error('Failed to delete comment:', error) } finally { setActiveCommentLoading(false) } }, [appId, comments, handleCommentIconClick, loadComments, setActiveComment, setActiveCommentId, setActiveCommentLoading, setCommentDetailCache]) const handleCommentPositionUpdate = useCallback(async (commentId: string, position: { x: number, y: number }) => { if (!appId) return const targetComment = comments.find(c => c.id === commentId) if (!targetComment) return const nextPosition = { position_x: position.x, position_y: position.y, } const previousComments = comments const updatedComments = comments.map(c => c.id === commentId ? { ...c, ...nextPosition } : c, ) setComments(updatedComments) const cachedDetail = commentDetailCacheRef.current[commentId] const updatedDetail = cachedDetail ? { ...cachedDetail, ...nextPosition } : null if (updatedDetail) { commentDetailCacheRef.current = { ...commentDetailCacheRef.current, [commentId]: updatedDetail, } setCommentDetailCache(commentDetailCacheRef.current) if (activeCommentIdRef.current === commentId) setActiveComment(updatedDetail) } else if (activeComment?.id === commentId) { setActiveComment({ ...activeComment, ...nextPosition }) } try { await updateWorkflowComment(appId, commentId, { content: targetComment.content, position_x: nextPosition.position_x, position_y: nextPosition.position_y, }) collaborationManager.emitCommentsUpdate(appId) } catch (error) { console.error('Failed to update comment position:', error) setComments(previousComments) if (cachedDetail) { commentDetailCacheRef.current = { ...commentDetailCacheRef.current, [commentId]: cachedDetail, } setCommentDetailCache(commentDetailCacheRef.current) if (activeCommentIdRef.current === commentId) setActiveComment(cachedDetail) } else if (activeComment?.id === commentId) { setActiveComment(activeComment) } } }, [activeComment, appId, comments, setComments, setCommentDetailCache, setActiveComment]) const handleCommentReply = useCallback(async (commentId: string, content: string, mentionedUserIds: string[] = []) => { if (!appId) return const trimmed = content.trim() if (!trimmed) return setReplySubmitting(true) try { await createWorkflowCommentReply(appId, commentId, { content: trimmed, mentioned_user_ids: mentionedUserIds }) collaborationManager.emitCommentsUpdate(appId) await refreshActiveComment(commentId) await loadComments() } catch (error) { console.error('Failed to create reply:', error) } finally { setReplySubmitting(false) } }, [appId, loadComments, refreshActiveComment, setReplySubmitting]) const handleCommentReplyUpdate = useCallback(async (commentId: string, replyId: string, content: string, mentionedUserIds: string[] = []) => { if (!appId) return const trimmed = content.trim() if (!trimmed) return setReplyUpdating(true) try { await updateWorkflowCommentReply(appId, commentId, replyId, { content: trimmed, mentioned_user_ids: mentionedUserIds }) collaborationManager.emitCommentsUpdate(appId) await refreshActiveComment(commentId) await loadComments() } catch (error) { console.error('Failed to update reply:', error) } finally { setReplyUpdating(false) } }, [appId, loadComments, refreshActiveComment, setReplyUpdating]) const handleCommentReplyDelete = useCallback(async (commentId: string, replyId: string) => { if (!appId) return setActiveCommentLoading(true) try { await deleteWorkflowCommentReply(appId, commentId, replyId) collaborationManager.emitCommentsUpdate(appId) await refreshActiveComment(commentId) await loadComments() } catch (error) { console.error('Failed to delete reply:', error) } finally { setActiveCommentLoading(false) } }, [appId, loadComments, refreshActiveComment, setActiveCommentLoading]) const handleCommentNavigate = useCallback((direction: 'prev' | 'next') => { const currentId = activeCommentIdRef.current if (!currentId) return const idx = comments.findIndex(c => c.id === currentId) if (idx === -1) return const target = direction === 'prev' ? comments[idx - 1] : comments[idx + 1] if (target) handleCommentIconClick(target) }, [comments, handleCommentIconClick]) const handleActiveCommentClose = useCallback(() => { setActiveComment(null) setActiveCommentLoading(false) setActiveCommentId(null) activeCommentIdRef.current = null }, [setActiveComment, setActiveCommentId, setActiveCommentLoading]) const handleCreateComment = useCallback((mousePosition: { pageX: number pageY: number elementX: number elementY: number }) => { if (controlMode === ControlMode.Comment) { console.log('Setting pending comment at screen position:', mousePosition) setPendingComment(mousePosition) } else { console.log('Control mode is not Comment:', controlMode) } }, [controlMode, setPendingComment]) return { comments, loading, pendingComment, activeComment, activeCommentLoading, replySubmitting, replyUpdating, handleCommentSubmit, handleCommentCancel, handleCommentIconClick, handleActiveCommentClose, handleCommentResolve, handleCommentDelete, handleCommentNavigate, handleCommentReply, handleCommentReplyUpdate, handleCommentReplyDelete, handleCommentPositionUpdate, refreshActiveComment, handleCreateComment, loadComments, } }