import { useRef, useState, useEffect, RefObject } from "react"; import { useClickAway } from "react-use"; import { useQueryClient } from "@tanstack/react-query"; import { searchFilesMentions } from "@/actions/mentions"; import { File } from "@/lib/type"; import { Braces, FileCode, FileText } from "lucide-react"; export function InputMentions({ ref, prompt, files, setPrompt, redesignMdUrl, onSubmit, }: { ref: RefObject; prompt: string; files?: File[] | null; redesignMdUrl?: string; setPrompt: (prompt: string) => void; onSubmit: () => void; }) { const queryClient = useQueryClient(); const [showMentionDropdown, setShowMentionDropdown] = useState(false); const [, setMentionSearch] = useState(""); const dropdownRef = useRef(null); const [results, setResults] = useState([]); useClickAway(dropdownRef, () => { setShowMentionDropdown(false); }); const getTextContent = (element: HTMLElement): string => { let text = ""; const childNodes = element.childNodes; for (let i = 0; i < childNodes.length; i++) { const node = childNodes[i]; if (node.nodeType === Node.TEXT_NODE) { text += node.textContent || ""; } else if (node.nodeType === Node.ELEMENT_NODE) { const el = node as HTMLElement; if (el.classList.contains("mention-chip")) { text += el.getAttribute("data-mention-id") || ""; } else { text += el.textContent || ""; } } } return text + "\u0020"; }; const extractPromptWithIds = (): string => { if (!ref.current) return ""; let text = ""; const childNodes = ref.current.childNodes; for (let i = 0; i < childNodes.length; i++) { const node = childNodes[i]; if (node.nodeType === Node.TEXT_NODE) { text += node.textContent || ""; } else if (node.nodeType === Node.ELEMENT_NODE) { const el = node as HTMLElement; if (el.classList.contains("mention-chip")) { text += el.getAttribute("data-mention-id") || ""; } else { text += el.textContent || ""; } } } return text; }; const shouldDetectMention = (): { detect: boolean; textBeforeCursor: string; } => { const selection = window.getSelection(); if (!selection || !ref.current) { return { detect: false, textBeforeCursor: "" }; } const range = selection.getRangeAt(0); const node = range.startContainer; if (node.nodeType === Node.ELEMENT_NODE) { const element = node as HTMLElement; if (element.classList?.contains("mention-chip")) { return { detect: false, textBeforeCursor: "" }; } } if (node.parentElement?.classList?.contains("mention-chip")) { return { detect: false, textBeforeCursor: "" }; } if (node.nodeType === Node.TEXT_NODE) { const textContent = node.textContent || ""; const cursorOffset = range.startOffset; const textBeforeCursor = textContent.substring(0, cursorOffset); const lastAtIndex = textBeforeCursor.lastIndexOf("@"); if (lastAtIndex !== -1) { const textAfterAt = textBeforeCursor.substring(lastAtIndex + 1); if (!textAfterAt.includes(" ")) { return { detect: true, textBeforeCursor: textAfterAt }; } } } return { detect: false, textBeforeCursor: "" }; }; const handleInput = async () => { if (!ref.current) return; const text = getTextContent(ref.current); if (text.trim() === "") { ref.current.innerHTML = ""; } setPrompt(text); const { detect, textBeforeCursor } = shouldDetectMention(); if (detect && files && files?.length > 0) { setMentionSearch(textBeforeCursor); setShowMentionDropdown(true); const files = queryClient.getQueryData(["files"]) ?? []; const results = await searchFilesMentions(textBeforeCursor, files); setResults(results); } else { setShowMentionDropdown(false); } }; const createMentionChipElement = (mentionId: string): HTMLSpanElement => { const mentionChip = document.createElement("span"); const baseClasses = "mention-chip inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium"; const typeClasses = "bg-indigo-500/10 text-indigo-500 dark:bg-indigo-500/20 dark:text-indigo-400"; mentionChip.className = `${baseClasses} ${typeClasses}`; mentionChip.contentEditable = "false"; mentionChip.setAttribute("data-mention-id", `file:/${mentionId}`); mentionChip.textContent = `@${mentionId}`; return mentionChip; }; const insertMention = (mentionId: string) => { if (!ref.current) return; const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) return; const range = selection.getRangeAt(0); const textNode = range.startContainer; if (textNode.nodeType !== Node.TEXT_NODE) return; const textContent = textNode.textContent || ""; const cursorOffset = range.startOffset; const textBeforeCursor = textContent.substring(0, cursorOffset); const lastAtIndex = textBeforeCursor.lastIndexOf("@"); if (lastAtIndex !== -1) { const mentionChip = createMentionChipElement(mentionId); const beforeText = textContent.substring(0, lastAtIndex); const afterText = textContent.substring(cursorOffset); const parent = textNode.parentNode; if (!parent) return; const beforeNode = beforeText ? document.createTextNode(beforeText) : null; const spaceNode = document.createTextNode("\u0020"); const afterNode = afterText ? document.createTextNode(afterText) : null; if (beforeNode) { parent.insertBefore(beforeNode, textNode); } parent.insertBefore(mentionChip, textNode); parent.insertBefore(spaceNode, textNode); if (afterNode) { parent.insertBefore(afterNode, textNode); } parent.removeChild(textNode); const newRange = document.createRange(); if (afterNode) { newRange.setStart(afterNode, 0); } else { newRange.setStartAfter(spaceNode); } newRange.collapse(true); selection.removeAllRanges(); selection.addRange(newRange); const newText = getTextContent(ref.current); setPrompt(newText); setShowMentionDropdown(false); setMentionSearch(""); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (!prompt || prompt.trim() === "") return; if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); const promptWithIds = extractPromptWithIds(); setPrompt(promptWithIds); onSubmit(); if (ref.current) { ref.current.innerHTML = ""; } setPrompt(""); setShowMentionDropdown(false); } else if (e.key === "Escape") { setShowMentionDropdown(false); } }; useEffect(() => { if (ref.current && prompt === "" && ref.current.innerHTML !== "") { ref.current.innerHTML = ""; } }, [prompt]); const handlePaste = (e: React.ClipboardEvent) => { e.preventDefault(); const text = e.clipboardData.getData("text/plain"); document.execCommand("insertText", false, text); }; return (
0 ? "Ask me anything. Type @ to mention a file..." : "Ask me anything..." } onInput={handleInput} onKeyDown={handleKeyDown} onPaste={handlePaste} suppressContentEditableWarning >
{showMentionDropdown && (
{results?.length > 0 && (
    {results.map((file) => ( insertMention(file.path)} /> ))}
)}
)}
); } export const getFileIcon = (filePath: string, size = "size-3.5") => { if (filePath.endsWith(".css")) { return ; } else if (filePath.endsWith(".js")) { return ; } else if (filePath.endsWith(".json")) { return ; } else { return ; } }; function MentionResult({ file, onSelect, }: { file: File; onSelect: () => void; }) { return (
  • {getFileIcon(file.path, "size-3")} {file.path}
  • ); }