import { SendOutlined } from "@ant-design/icons"; import { useRef, type CSSProperties, type Dispatch, type SetStateAction } from "react"; import type { CanvasNodeKind, CanvasPromptMentionOption, CanvasPromptMentionState } from "./canvasTypes"; const MENTION_BOUNDARY_RE = /\s|[,。、;:!??(){}[\]<>]/; const EMPTY_MENTION_STYLE: CSSProperties = { opacity: 0.5, pointerEvents: "none" }; const DEFAULT_MENTION_STATE: CanvasPromptMentionState = { open: false, query: "", start: 0, caret: 0, activeIndex: 0, }; interface CanvasPromptMentionTextareaProps { nodeId: string; value: string; placeholder: string; mentionOptions: CanvasPromptMentionOption[]; mentionState?: CanvasPromptMentionState; onPromptChange: (nodeId: string, prompt: string) => void; onMentionStateChange: Dispatch>>; onCloseMention: (nodeId: string) => void; onInsertMention: ( nodeId: string, option: CanvasPromptMentionOption, textarea: HTMLTextAreaElement | null, kind?: CanvasNodeKind, ) => void; mentionKind?: CanvasNodeKind; } export function CanvasPromptMentionTextarea({ nodeId, value, placeholder, mentionOptions, mentionState = DEFAULT_MENTION_STATE, onPromptChange, onMentionStateChange, onCloseMention, onInsertMention, mentionKind, }: CanvasPromptMentionTextareaProps) { const textareaRef = useRef(null); const filteredMentions = mentionState.open ? mentionOptions.filter((option) => !mentionState.query || option.searchText.includes(mentionState.query.toLowerCase())) : []; const handlePromptChange = (event: React.ChangeEvent) => { const value = event.target.value; const caret = event.target.selectionStart || 0; onPromptChange(nodeId, value); const beforeCaret = value.slice(0, caret); const atIndex = beforeCaret.lastIndexOf("@"); if (atIndex >= 0) { const query = beforeCaret.slice(atIndex + 1); if (!MENTION_BOUNDARY_RE.test(query) && !query.includes(" ")) { onMentionStateChange((prev) => ({ ...prev, [nodeId]: { open: true, query, start: atIndex, caret, activeIndex: 0 }, })); return; } } onCloseMention(nodeId); }; const handlePromptKeyDown = (event: React.KeyboardEvent) => { if (!mentionState.open || filteredMentions.length === 0) return; if (event.key === "ArrowDown") { event.preventDefault(); onMentionStateChange((prev) => ({ ...prev, [nodeId]: { ...mentionState, activeIndex: (mentionState.activeIndex + 1) % filteredMentions.length }, })); } else if (event.key === "ArrowUp") { event.preventDefault(); onMentionStateChange((prev) => ({ ...prev, [nodeId]: { ...mentionState, activeIndex: (mentionState.activeIndex - 1 + filteredMentions.length) % filteredMentions.length, }, })); } else if (event.key === "Enter" || event.key === "Tab") { event.preventDefault(); const option = filteredMentions[mentionState.activeIndex]; if (option) { onInsertMention(nodeId, option, event.currentTarget, mentionKind); } } else if (event.key === "Escape") { event.preventDefault(); onCloseMention(nodeId); } }; const handlePromptSelect = (event: React.SyntheticEvent) => { const caret = event.currentTarget.selectionStart || 0; onMentionStateChange((prev) => { const current = prev[nodeId]; if (!current?.open) return prev; return { ...prev, [nodeId]: { ...current, caret } }; }); }; return (