Compare commits

...
Author SHA1 Message Date
bflatastic 5a8612d57e Fix @mention tagging in prompts with CJK characters
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.
2026-04-16 06:51:05 +07:00
@@ -348,15 +348,26 @@ export const MentionTextarea = forwardRef<HTMLDivElement, MentionTextareaProps>(
}, [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<string, string> = {};
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<HTMLDivElement, MentionTextareaProps>(
// 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<HTMLDivElement, MentionTextareaProps>(
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<HTMLDivElement, MentionTextareaProps>(
}, []);
// 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")) {