diff --git a/web/app/components/workflow/comment/index.tsx b/web/app/components/workflow/comment/index.tsx index 148d5094f8..7c91b8dccb 100644 --- a/web/app/components/workflow/comment/index.tsx +++ b/web/app/components/workflow/comment/index.tsx @@ -2,3 +2,4 @@ export { CommentCursor } from './cursor' export { CommentInput } from './input' export { CommentIcon } from './icon' export { CommentThread } from './thread' +export { MentionInput } from './mention-input' diff --git a/web/app/components/workflow/comment/input.tsx b/web/app/components/workflow/comment/input.tsx index ac20064839..231f13c99b 100644 --- a/web/app/components/workflow/comment/input.tsx +++ b/web/app/components/workflow/comment/input.tsx @@ -1,15 +1,10 @@ import type { FC } from 'react' -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { createPortal } from 'react-dom' -import Textarea from 'react-textarea-autosize' -import { RiSendPlane2Fill } from '@remixicon/react' -import { useParams } from 'next/navigation' +import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { useReactFlow, useViewport } from 'reactflow' -import cn from '@/utils/classnames' -import Button from '@/app/components/base/button' import Avatar from '@/app/components/base/avatar' import { useAppContext } from '@/context/app-context' -import { type UserProfile, fetchMentionableUsers } from '@/service/workflow-comment' +import { MentionInput } from './mention-input' +import cn from '@/utils/classnames' type CommentInputProps = { position: { x: number; y: number } @@ -19,39 +14,14 @@ type CommentInputProps = { export const CommentInput: FC = memo(({ position, onSubmit, onCancel }) => { const [content, setContent] = useState('') - const textareaRef = useRef(null) const { userProfile } = useAppContext() const { flowToScreenPosition } = useReactFlow() const viewport = useViewport() - const params = useParams() - const appId = params.appId as string - - const [mentionUsers, setMentionUsers] = useState([]) - const [showMentionDropdown, setShowMentionDropdown] = useState(false) - const [mentionQuery, setMentionQuery] = useState('') - const [mentionPosition, setMentionPosition] = useState(0) - const [selectedMentionIndex, setSelectedMentionIndex] = useState(0) - const [mentionedUserIds, setMentionedUserIds] = useState([]) const screenPosition = useMemo(() => { return flowToScreenPosition(position) }, [position.x, position.y, viewport.x, viewport.y, viewport.zoom, flowToScreenPosition]) - const loadMentionableUsers = useCallback(async () => { - if (!appId) return - try { - const users = await fetchMentionableUsers(appId) - setMentionUsers(users) - } - catch (error) { - console.error('Failed to load mentionable users:', error) - } - }, [appId]) - - useEffect(() => { - loadMentionableUsers() - }, [loadMentionableUsers]) - useEffect(() => { const handleGlobalKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { @@ -67,142 +37,10 @@ export const CommentInput: FC = memo(({ position, onSubmit, o } }, [onCancel]) - const filteredMentionUsers = useMemo(() => { - if (!mentionQuery) return mentionUsers - return mentionUsers.filter(user => - user.name.toLowerCase().includes(mentionQuery.toLowerCase()) - || user.email.toLowerCase().includes(mentionQuery.toLowerCase()), - ) - }, [mentionUsers, mentionQuery]) - - const dropdownPosition = useMemo(() => { - if (!showMentionDropdown || !textareaRef.current) - return { x: 0, y: 0 } - - const textareaRect = textareaRef.current.getBoundingClientRect() - return { - x: textareaRect.left, - y: textareaRect.bottom + 4, - } - }, [showMentionDropdown]) - - const handleContentChange = useCallback((value: string) => { - setContent(value) - - // Delay getting cursor position to ensure the textarea has updated - setTimeout(() => { - const cursorPosition = textareaRef.current?.selectionStart || 0 - const textBeforeCursor = value.slice(0, cursorPosition) - const mentionMatch = textBeforeCursor.match(/@(\w*)$/) - - if (mentionMatch) { - setMentionQuery(mentionMatch[1]) - setMentionPosition(cursorPosition - mentionMatch[0].length) - setShowMentionDropdown(true) - setSelectedMentionIndex(0) - } - else { - setShowMentionDropdown(false) - } - }, 0) - }, []) - - const handleMentionButtonClick = useCallback((e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() - console.log('Mention button clicked!') - - const textarea = textareaRef.current - if (!textarea) return - - const cursorPosition = textarea.selectionStart || 0 - const newContent = `${content.slice(0, cursorPosition)}@${content.slice(cursorPosition)}` - - setContent(newContent) - - setTimeout(() => { - const newCursorPos = cursorPosition + 1 - textarea.setSelectionRange(newCursorPos, newCursorPos) - textarea.focus() - - setMentionQuery('') - setMentionPosition(cursorPosition) - setShowMentionDropdown(true) - setSelectedMentionIndex(0) - }, 0) - }, [content]) - - const insertMention = useCallback((user: UserProfile) => { - const textarea = textareaRef.current - if (!textarea) return - - const beforeMention = content.slice(0, mentionPosition) - const afterMention = content.slice(textarea.selectionStart || 0) - const newContent = `${beforeMention}@${user.name} ${afterMention}` - - setContent(newContent) - setShowMentionDropdown(false) - setMentionedUserIds(prev => [...prev, user.id]) - - setTimeout(() => { - const newCursorPos = mentionPosition + user.name.length + 2 // @ + name + space - textarea.setSelectionRange(newCursorPos, newCursorPos) - textarea.focus() - }, 0) - }, [content, mentionPosition]) - - const handleSubmit = useCallback((e?: React.MouseEvent) => { - if (e) { - e.preventDefault() - e.stopPropagation() - } - console.log('Submit button clicked!') - - try { - if (content.trim()) { - onSubmit(content.trim(), mentionedUserIds) - setContent('') - setMentionedUserIds([]) - } - } - catch (error) { - console.error('Error in CommentInput handleSubmit:', error) - } - }, [content, mentionedUserIds, onSubmit]) - - const handleKeyDown = useCallback((e: React.KeyboardEvent) => { - if (showMentionDropdown) { - if (e.key === 'ArrowDown') { - e.preventDefault() - setSelectedMentionIndex(prev => - prev < filteredMentionUsers.length - 1 ? prev + 1 : 0, - ) - } - else if (e.key === 'ArrowUp') { - e.preventDefault() - setSelectedMentionIndex(prev => - prev > 0 ? prev - 1 : filteredMentionUsers.length - 1, - ) - } - else if (e.key === 'Enter') { - e.preventDefault() - if (filteredMentionUsers[selectedMentionIndex]) - insertMention(filteredMentionUsers[selectedMentionIndex]) - - return - } - else if (e.key === 'Escape') { - e.preventDefault() - setShowMentionDropdown(false) - return - } - } - - if (e.key === 'Enter' && !e.shiftKey && !showMentionDropdown) { - e.preventDefault() - handleSubmit() - } - }, [showMentionDropdown, filteredMentionUsers, selectedMentionIndex, insertMention, handleSubmit]) + const handleMentionSubmit = useCallback((content: string, mentionedUserIds: string[]) => { + onSubmit(content, mentionedUserIds) + setContent('') + }, [onSubmit]) return (
= memo(({ position, onSubmit, o )} >
-
-
-