mirror of https://github.com/langgenius/dify.git
add create comment frontend
This commit is contained in:
parent
1721314c62
commit
75257232c3
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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']}>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export enum BlockEnum {
|
|||
export enum ControlMode {
|
||||
Pointer = 'pointer',
|
||||
Hand = 'hand',
|
||||
Comment = 'comment',
|
||||
}
|
||||
export enum ErrorHandleMode {
|
||||
Terminated = 'terminated',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ const translation = {
|
|||
pasteHere: '粘贴到这里',
|
||||
pointerMode: '指针模式',
|
||||
handMode: '手模式',
|
||||
commentMode: '评论模式',
|
||||
exportImage: '导出图片',
|
||||
exportPNG: '导出为 PNG',
|
||||
exportJPEG: '导出为 JPEG',
|
||||
|
|
|
|||
Loading…
Reference in New Issue