'use client' import type { FC } from 'react' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { useParams } from 'next/navigation' import { RiArrowUpLine, RiAtLine } from '@remixicon/react' import Textarea from 'react-textarea-autosize' import Button from '@/app/components/base/button' import Avatar from '@/app/components/base/avatar' import cn from '@/utils/classnames' import { type UserProfile, fetchMentionableUsers } from '@/service/workflow-comment' type MentionInputProps = { value: string onChange: (value: string) => void onSubmit: (content: string, mentionedUserIds: string[]) => void onCancel?: () => void placeholder?: string disabled?: boolean loading?: boolean className?: string isEditing?: boolean autoFocus?: boolean } export const MentionInput: FC = memo(({ value, onChange, onSubmit, onCancel, placeholder = 'Add a comment', disabled = false, loading = false, className, isEditing = false, autoFocus = false, }) => { const params = useParams() const appId = params.appId as string const textareaRef = useRef(null) 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 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]) 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((newValue: string) => { onChange(newValue) setTimeout(() => { const cursorPosition = textareaRef.current?.selectionStart || 0 const textBeforeCursor = newValue.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) }, [onChange]) const handleMentionButtonClick = useCallback((e: React.MouseEvent) => { e.preventDefault() e.stopPropagation() const textarea = textareaRef.current if (!textarea) return const cursorPosition = textarea.selectionStart || 0 const newContent = `${value.slice(0, cursorPosition)}@${value.slice(cursorPosition)}` onChange(newContent) setTimeout(() => { const newCursorPos = cursorPosition + 1 textarea.setSelectionRange(newCursorPos, newCursorPos) textarea.focus() setMentionQuery('') setMentionPosition(cursorPosition) setShowMentionDropdown(true) setSelectedMentionIndex(0) }, 0) }, [value, onChange]) const insertMention = useCallback((user: UserProfile) => { const textarea = textareaRef.current if (!textarea) return const beforeMention = value.slice(0, mentionPosition) const afterMention = value.slice(textarea.selectionStart || 0) const newContent = `${beforeMention}@${user.name} ${afterMention}` onChange(newContent) setShowMentionDropdown(false) const newMentionedUserIds = [...mentionedUserIds, user.id] setMentionedUserIds(newMentionedUserIds) setTimeout(() => { const newCursorPos = mentionPosition + user.name.length + 2 // @ + name + space textarea.setSelectionRange(newCursorPos, newCursorPos) textarea.focus() }, 0) }, [value, mentionPosition, onChange, mentionedUserIds]) const handleSubmit = useCallback((e?: React.MouseEvent) => { if (e) { e.preventDefault() e.stopPropagation() } if (value.trim()) { onSubmit(value.trim(), mentionedUserIds) setMentionedUserIds([]) setShowMentionDropdown(false) } }, [value, 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 resetMentionState = useCallback(() => { setMentionedUserIds([]) setShowMentionDropdown(false) setMentionQuery('') setMentionPosition(0) setSelectedMentionIndex(0) }, []) useEffect(() => { if (!value) resetMentionState() }, [value, resetMentionState]) useEffect(() => { if (autoFocus && textareaRef.current) { const textarea = textareaRef.current setTimeout(() => { textarea.focus() const length = textarea.value.length textarea.setSelectionRange(length, length) }, 0) } }, [autoFocus]) return ( <>