"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { trpc } from "~/lib/trpc/client.js"; interface CommentInputProps { entityType: string; entityId: string; parentId?: string; onSubmit: (body: string) => void; onCancel?: () => void; isSubmitting?: boolean; placeholder?: string; autoFocus?: boolean; } interface MentionCandidate { id: string; name: string | null; email: string; } export function CommentInput({ entityType: _entityType, entityId: _entityId, parentId: _parentId, onSubmit, onCancel, isSubmitting = false, placeholder = "Write a comment... Use @ to mention someone", autoFocus = false, }: CommentInputProps) { const [body, setBody] = useState(""); const [mentionQuery, setMentionQuery] = useState(null); const [mentionIndex, setMentionIndex] = useState(0); const [cursorPosition, setCursorPosition] = useState(0); const textareaRef = useRef(null); // Fetch users for mention autocomplete (only when needed) const usersQuery = trpc.user.listAssignable.useQuery(undefined, { enabled: mentionQuery !== null, staleTime: 60_000, }); const users = usersQuery.data ?? []; // Filter users based on mention query const filteredUsers: MentionCandidate[] = mentionQuery !== null ? users.filter((u) => { const q = mentionQuery.toLowerCase(); return ( (u.name?.toLowerCase().includes(q) ?? false) || u.email.toLowerCase().includes(q) ); }).slice(0, 8) : []; // Reset mention index when filtered list changes useEffect(() => { setMentionIndex(0); }, [mentionQuery]); const handleChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; const cursor = e.target.selectionStart ?? value.length; setBody(value); setCursorPosition(cursor); // Detect if we are in a @mention context const textBeforeCursor = value.slice(0, cursor); const atMatch = textBeforeCursor.match(/@([^\s@]*)$/); if (atMatch) { setMentionQuery(atMatch[1]!); } else { setMentionQuery(null); } }, [], ); const insertMention = useCallback( (user: MentionCandidate) => { const textBeforeCursor = body.slice(0, cursorPosition); const textAfterCursor = body.slice(cursorPosition); // Find the @ that triggered this mention const atMatch = textBeforeCursor.match(/@([^\s@]*)$/); if (!atMatch) return; const atStart = textBeforeCursor.length - atMatch[0].length; const displayName = user.name ?? user.email; const mentionText = `@[${displayName}](${user.id}) `; const newBody = textBeforeCursor.slice(0, atStart) + mentionText + textAfterCursor; setBody(newBody); setMentionQuery(null); // Focus and set cursor position const newCursorPos = atStart + mentionText.length; requestAnimationFrame(() => { const ta = textareaRef.current; if (ta) { ta.focus(); ta.selectionStart = newCursorPos; ta.selectionEnd = newCursorPos; } }); }, [body, cursorPosition], ); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { // Handle mention dropdown navigation if (mentionQuery !== null && filteredUsers.length > 0) { if (e.key === "ArrowDown") { e.preventDefault(); setMentionIndex((prev) => prev < filteredUsers.length - 1 ? prev + 1 : 0, ); return; } if (e.key === "ArrowUp") { e.preventDefault(); setMentionIndex((prev) => prev > 0 ? prev - 1 : filteredUsers.length - 1, ); return; } if (e.key === "Enter" || e.key === "Tab") { e.preventDefault(); insertMention(filteredUsers[mentionIndex]!); return; } if (e.key === "Escape") { e.preventDefault(); setMentionQuery(null); return; } } // Submit on Ctrl+Enter / Cmd+Enter if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); if (body.trim().length > 0 && !isSubmitting) { onSubmit(body.trim()); setBody(""); } } }, [mentionQuery, filteredUsers, mentionIndex, insertMention, body, isSubmitting, onSubmit], ); function handleSubmitClick() { if (body.trim().length > 0 && !isSubmitting) { onSubmit(body.trim()); setBody(""); } } return (