From f94fc5bf078d81133adaf70d5edbc690affb0454 Mon Sep 17 00:00:00 2001 From: BFlat Date: Thu, 16 Apr 2026 07:05:21 +0700 Subject: [PATCH] Fix @mention tagging in prompts with CJK characters (#1376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs were preventing @image / @video / @character references from being tagged in prompts written with Chinese, Japanese, or Korean text: 1. The mention-highlight regex was built with just the "g" flag, so a user-typed "@image6" never matched the canonical label "@Image6" and the reference was rendered as plain text. Make the regex case-insensitive and do the colorMap lookup via a lowercase-keyed table so "@image6" still finds the color for "@Image6". 2. detectMention required the character immediately before "@" to match /\s/, which never matches CJK characters. Typing "@" right after a Chinese character like "从" therefore never opened the autocomplete dropdown, leaving users to type the mention by hand (which then hit bug 1). Replace the whitespace check with a "not an ASCII identifier char" check so CJK, quotes, and punctuation all count as valid word boundaries before an @mention. --- .../promptbox/src/lib/MentionTextarea.tsx | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/frontend/libs/components/promptbox/src/lib/MentionTextarea.tsx b/frontend/libs/components/promptbox/src/lib/MentionTextarea.tsx index 7ea3e897e1..7f6c1bf2a8 100644 --- a/frontend/libs/components/promptbox/src/lib/MentionTextarea.tsx +++ b/frontend/libs/components/promptbox/src/lib/MentionTextarea.tsx @@ -348,15 +348,26 @@ export const MentionTextarea = forwardRef( }, [mentionItems, mentionState.isOpen, mentionState.query]); // Build a regex that matches any known mention label (supports spaces in names) - // Sort longest-first so "@Pumpkin Head" matches before "@Pumpkin" + // Sort longest-first so "@Pumpkin Head" matches before "@Pumpkin". + // Case-insensitive so users can type "@image1" and still get it tagged + // when the canonical label is "@Image1". const mentionRegex = useMemo(() => { const labels = mentionItems.map((item) => item.label); if (labels.length === 0) return null; const sorted = [...labels].sort((a, b) => b.length - a.length); const pattern = sorted.map((l) => escapeRegex(l)).join("|"); - return new RegExp(`(${pattern})`, "g"); + return new RegExp(`(${pattern})`, "gi"); }, [mentionItems]); + // Case-insensitive lookup table for colorMap so "@image1" finds "@Image1". + const lowerColorMap = useMemo(() => { + const m: Record = {}; + for (const [k, v] of Object.entries(colorMap)) { + m[k.toLowerCase()] = v; + } + return m; + }, [colorMap]); + // Build innerHTML with colored @mentions inline const buildHTML = useCallback( (text: string): string => { @@ -376,7 +387,7 @@ export const MentionTextarea = forwardRef( // biome-ignore lint/suspicious/noAssignInExpressions: -- while ((match = regex.exec(text)) !== null) { const fullMatch = match[0]; - const color = colorMap[fullMatch]; + const color = lowerColorMap[fullMatch.toLowerCase()]; if (match.index > lastIndex) { html += escapeHTML(text.slice(lastIndex, match.index)); @@ -402,7 +413,7 @@ export const MentionTextarea = forwardRef( return html; }, - [colorMap, mentionRegex], + [lowerColorMap, mentionRegex], ); // Sync DOM when value changes from parent (not from user input) @@ -464,13 +475,20 @@ export const MentionTextarea = forwardRef( }, []); // Detect @mention trigger from cursor position - // Supports multi-word names by scanning back to the nearest @ + // Supports multi-word names by scanning back to the nearest @. + // A valid trigger requires the char before @ to not be an ASCII + // identifier char (letter/digit/underscore) — this lets CJK characters, + // quotes, and punctuation all count as word boundaries, so users can + // type "@Image1" directly after Chinese text like 从@Image1. const detectMention = useCallback( (text: string, cursorPos: number) => { // Find the last @ before cursor const textBefore = text.slice(0, cursorPos); const lastAt = textBefore.lastIndexOf("@"); - if (lastAt !== -1 && (lastAt === 0 || /\s/.test(text[lastAt - 1]))) { + if ( + lastAt !== -1 && + (lastAt === 0 || !/[A-Za-z0-9_]/.test(text[lastAt - 1])) + ) { const query = text.slice(lastAt, cursorPos); // includes @ // Only open if there's no newline in the query if (!query.includes("\n")) {