add create comment frontend

This commit is contained in:
hjlarry 2025-09-14 12:10:37 +08:00
parent 1721314c62
commit 75257232c3
11 changed files with 303 additions and 3 deletions

View File

@ -0,0 +1,21 @@
import { useEventListener } from 'ahooks'
import { useWorkflowStore } from './store'
import { useWorkflowComment } from './hooks/use-workflow-comment'
const CommentManager = () => {
const workflowStore = useWorkflowStore()
const { handleCreateComment } = useWorkflowComment()
useEventListener('click', (e) => {
const { controlMode, mousePosition } = workflowStore.getState()
if (controlMode === 'comment') {
e.preventDefault()
handleCreateComment(mousePosition)
}
})
return null
}
export default CommentManager

View File

@ -0,0 +1,128 @@
import type { FC } from 'react'
import { memo, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiMessage3Line } from '@remixicon/react'
import { useStore } from '../store'
import { ControlMode } from '../types'
import type { WorkflowComment } from '@/service/workflow-comment'
type CommentCursorProps = {
mousePosition: { elementX: number; elementY: number }
}
export const CommentCursor: FC<CommentCursorProps> = memo(({ mousePosition }) => {
const controlMode = useStore(s => s.controlMode)
if (controlMode !== ControlMode.Comment)
return null
return (
<div
className="pointer-events-none absolute z-50 flex h-6 w-6 items-center justify-center rounded bg-blue-500 text-white shadow-lg"
style={{
left: mousePosition.elementX - 3,
top: mousePosition.elementY - 3,
transform: 'translate(-50%, -50%)',
}}
>
<RiMessage3Line className="h-4 w-4" />
</div>
)
})
CommentCursor.displayName = 'CommentCursor'
type CommentInputProps = {
position: { x: number; y: number }
onSubmit: (content: string) => void
onCancel: () => void
}
export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, onCancel }) => {
const { t } = useTranslation()
const [content, setContent] = useState('')
const handleSubmit = useCallback(() => {
try {
if (content.trim()) {
onSubmit(content.trim())
setContent('')
}
}
catch (error) {
console.error('Error in CommentInput handleSubmit:', error)
}
}, [content, onSubmit])
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSubmit()
}
else if (e.key === 'Escape') {
onCancel()
}
}, [handleSubmit, onCancel])
return (
<div
className="absolute z-50 w-64 rounded-lg border bg-white shadow-lg"
style={{
left: position.x + 10,
top: position.y + 10,
}}
>
<textarea
autoFocus
value={content}
onChange={e => setContent(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Add comment..."
className="w-full resize-none rounded-t-lg border-0 p-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={3}
/>
<div className="flex justify-end gap-2 border-t bg-gray-50 p-2">
<button
onClick={onCancel}
className="rounded px-3 py-1 text-sm text-gray-500 hover:bg-gray-100"
>
Cancel
</button>
<button
onClick={handleSubmit}
disabled={!content.trim()}
className="rounded bg-blue-500 px-3 py-1 text-sm text-white hover:bg-blue-600 disabled:bg-gray-300"
>
Submit
</button>
</div>
</div>
)
})
CommentInput.displayName = 'CommentInput'
type CommentIconProps = {
comment: WorkflowComment
onClick: () => void
}
export const CommentIcon: FC<CommentIconProps> = memo(({ comment, onClick }) => {
return (
<div
className="absolute z-40 flex h-8 w-8 cursor-pointer items-center justify-center rounded-full bg-blue-500 text-white shadow-lg hover:bg-blue-600"
style={{
left: comment.position_x,
top: comment.position_y,
transform: 'translate(-50%, -50%)',
}}
onClick={onClick}
>
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-medium text-blue-500">
TEST
</div>
</div>
)
})
CommentIcon.displayName = 'CommentIcon'

View File

@ -19,3 +19,4 @@ export * from './use-format-time-from-now'
export * from './use-workflow-refresh-draft'
export * from './use-inspect-vars-crud'
export * from './use-set-workflow-vars-with-value'
export * from './use-workflow-comment'

View File

@ -0,0 +1,86 @@
import { useCallback, useState } from 'react'
import { useParams } from 'next/navigation'
import { useStore } from '../store'
import { ControlMode } from '../types'
import type { WorkflowComment } from '@/service/workflow-comment'
import { createWorkflowComment } from '@/service/workflow-comment'
export const useWorkflowComment = () => {
const params = useParams()
const appId = params.appId as string
const controlMode = useStore(s => s.controlMode)
const setControlMode = useStore(s => s.setControlMode)
const pendingComment = useStore(s => s.pendingComment)
const setPendingComment = useStore(s => s.setPendingComment)
const [comments, setComments] = useState<WorkflowComment[]>([])
const handleCommentSubmit = useCallback(async (content: string) => {
if (!pendingComment) return
console.log('Submitting comment:', { appId, pendingComment, content })
if (!appId) {
console.error('AppId is missing')
return
}
try {
const newComment = await createWorkflowComment(appId, {
position_x: pendingComment.x,
position_y: pendingComment.y,
content,
mentioned_user_ids: [],
})
console.log('Comment created successfully:', newComment)
setComments(prev => [...prev, newComment])
setPendingComment(null)
setControlMode(ControlMode.Pointer)
}
catch (error) {
console.error('Failed to create comment:', error)
setPendingComment(null)
setControlMode(ControlMode.Pointer)
}
}, [appId, pendingComment, setControlMode, setPendingComment, setComments])
const handleCommentCancel = useCallback(() => {
setPendingComment(null)
setControlMode(ControlMode.Pointer)
}, [setControlMode, setPendingComment])
const handleCommentIconClick = useCallback((comment: WorkflowComment) => {
// TODO: display comment details
console.log('Comment clicked:', comment)
}, [])
const handleCreateComment = useCallback((mousePosition: { pageX: number; pageY: number }) => {
if (controlMode === ControlMode.Comment) {
const containerElement = document.querySelector('#workflow-container')
if (containerElement) {
const containerBounds = containerElement.getBoundingClientRect()
const position = {
x: mousePosition.pageX - containerBounds.left,
y: mousePosition.pageY - containerBounds.top,
}
console.log('Setting pending comment at position:', position)
setPendingComment(position)
}
else {
console.error('Could not find workflow container element')
}
}
else {
console.log('Control mode is not Comment:', controlMode)
}
}, [controlMode, setPendingComment])
return {
comments,
pendingComment,
handleCommentSubmit,
handleCommentCancel,
handleCommentIconClick,
handleCreateComment,
}
}

View File

@ -63,10 +63,13 @@ import CustomEdge from './custom-edge'
import CustomConnectionLine from './custom-connection-line'
import HelpLine from './help-line'
import CandidateNode from './candidate-node'
import CommentManager from './comment-manager'
import PanelContextmenu from './panel-contextmenu'
import NodeContextmenu from './node-contextmenu'
import SyncingDataModal from './syncing-data-modal'
import LimitTips from './limit-tips'
import { CommentCursor, CommentIcon, CommentInput } from './comment'
import { useWorkflowComment } from './hooks/use-workflow-comment'
import {
useStore,
useWorkflowStore,
@ -156,6 +159,14 @@ export const Workflow: FC<WorkflowProps> = memo(({
const { workflowReadOnly } = useWorkflowReadOnly()
const { nodesReadOnly } = useNodesReadOnly()
const { eventEmitter } = useEventEmitterContextContext()
const {
comments,
pendingComment,
handleCommentSubmit,
handleCommentCancel,
handleCommentIconClick,
} = useWorkflowComment()
const mousePosition = useStore(s => s.mousePosition)
eventEmitter?.useSubscription((v: any) => {
if (v.type === WORKFLOW_DATA_UPDATE) {
@ -297,11 +308,13 @@ export const Workflow: FC<WorkflowProps> = memo(({
relative h-full w-full min-w-[960px]
${workflowReadOnly && 'workflow-panel-animation'}
${nodeAnimation && 'workflow-node-animation'}
${controlMode === ControlMode.Comment ? 'cursor-crosshair' : ''}
`}
ref={workflowContainerRef}
>
<SyncingDataModal />
<CandidateNode />
<CommentManager />
<div
className='absolute left-0 top-0 z-10 flex w-12 items-center justify-center p-1 pl-2'
style={{ height: controlHeight }}
@ -324,6 +337,21 @@ export const Workflow: FC<WorkflowProps> = memo(({
)
}
<LimitTips />
<CommentCursor mousePosition={mousePosition} />
{pendingComment && (
<CommentInput
position={pendingComment}
onSubmit={handleCommentSubmit}
onCancel={handleCommentCancel}
/>
)}
{comments.map(comment => (
<CommentIcon
key={comment.id}
comment={comment}
onClick={() => handleCommentIconClick(comment)}
/>
))}
{children}
<ReactFlow
nodeTypes={nodeTypes}

View File

@ -9,6 +9,7 @@ import {
RiCursorLine,
RiFunctionAddLine,
RiHand,
RiMessage3Line,
RiStickyNoteAddLine,
} from '@remixicon/react'
import {
@ -34,7 +35,7 @@ const Control = () => {
const maximizeCanvas = useStore(s => s.maximizeCanvas)
const { handleModePointer, handleModeHand } = useWorkflowMoveMode()
const { handleLayout } = useWorkflowOrganize()
const { handleAddNote } = useOperator()
const { handleAddNote, handleAddComment } = useOperator()
const {
nodesReadOnly,
getNodesReadOnly,
@ -49,6 +50,14 @@ const Control = () => {
handleAddNote()
}
const addComment = (e: MouseEvent<HTMLDivElement>) => {
if (getNodesReadOnly())
return
e.stopPropagation()
handleAddComment()
}
return (
<div className='flex flex-col items-center rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 text-text-tertiary shadow-lg'>
<AddBlock />
@ -88,6 +97,18 @@ const Control = () => {
<RiHand className='h-4 w-4' />
</div>
</TipPopup>
<TipPopup title={t('workflow.common.commentMode')} shortcuts={['c']}>
<div
className={cn(
'ml-[1px] flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg',
controlMode === ControlMode.Comment ? 'bg-state-accent-active text-text-accent' : 'hover:bg-state-base-hover hover:text-text-secondary',
`${nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled'}`,
)}
onClick={addComment}
>
<RiMessage3Line className='h-4 w-4' />
</div>
</TipPopup>
<Divider className='my-1 w-3.5' />
<ExportImage />
<TipPopup title={t('workflow.panel.organizeBlocks')} shortcuts={['ctrl', 'o']}>

View File

@ -5,6 +5,7 @@ import type { NoteNodeType } from '../note-node/types'
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
import { NoteTheme } from '../note-node/types'
import { useAppContext } from '@/context/app-context'
import { ControlMode } from '../types'
export const useOperator = () => {
const workflowStore = useWorkflowStore()
@ -35,7 +36,14 @@ export const useOperator = () => {
})
}, [workflowStore, userProfile])
const handleAddComment = useCallback(() => {
workflowStore.setState({
controlMode: ControlMode.Comment,
})
}, [workflowStore])
return {
handleAddNote,
handleAddComment,
}
}

View File

@ -18,8 +18,10 @@ export type WorkflowSliceShape = {
setSelection: (selection: WorkflowSliceShape['selection']) => void
bundleNodeSize: { width: number; height: number } | null
setBundleNodeSize: (bundleNodeSize: WorkflowSliceShape['bundleNodeSize']) => void
controlMode: 'pointer' | 'hand'
controlMode: 'pointer' | 'hand' | 'comment'
setControlMode: (controlMode: WorkflowSliceShape['controlMode']) => void
pendingComment: { x: number; y: number } | null
setPendingComment: (pendingComment: WorkflowSliceShape['pendingComment']) => void
mousePosition: { pageX: number; pageY: number; elementX: number; elementY: number }
setMousePosition: (mousePosition: WorkflowSliceShape['mousePosition']) => void
showConfirm?: { title: string; desc?: string; onConfirm: () => void }
@ -43,11 +45,13 @@ export const createWorkflowSlice: StateCreator<WorkflowSliceShape> = set => ({
setSelection: selection => set(() => ({ selection })),
bundleNodeSize: null,
setBundleNodeSize: bundleNodeSize => set(() => ({ bundleNodeSize })),
controlMode: localStorage.getItem('workflow-operation-mode') === 'pointer' ? 'pointer' : 'hand',
controlMode: localStorage.getItem('workflow-operation-mode') === 'pointer' ? 'pointer' : localStorage.getItem('workflow-operation-mode') === 'hand' ? 'hand' : 'comment',
setControlMode: (controlMode) => {
set(() => ({ controlMode }))
localStorage.setItem('workflow-operation-mode', controlMode)
},
pendingComment: null,
setPendingComment: pendingComment => set(() => ({ pendingComment })),
mousePosition: { pageX: 0, pageY: 0, elementX: 0, elementY: 0 },
setMousePosition: mousePosition => set(() => ({ mousePosition })),
showConfirm: undefined,

View File

@ -47,6 +47,7 @@ export enum BlockEnum {
export enum ControlMode {
Pointer = 'pointer',
Hand = 'hand',
Comment = 'comment',
}
export enum ErrorHandleMode {
Terminated = 'terminated',

View File

@ -70,6 +70,7 @@ const translation = {
pasteHere: 'Paste Here',
pointerMode: 'Pointer Mode',
handMode: 'Hand Mode',
commentMode: 'Comment Mode',
exportImage: 'Export Image',
exportPNG: 'Export as PNG',
exportJPEG: 'Export as JPEG',

View File

@ -69,6 +69,7 @@ const translation = {
pasteHere: '粘贴到这里',
pointerMode: '指针模式',
handMode: '手模式',
commentMode: '评论模式',
exportImage: '导出图片',
exportPNG: '导出为 PNG',
exportJPEG: '导出为 JPEG',