mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-07 02:21:23 +00:00
Fix @mention tagging in prompts with CJK characters (#1376)
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.
This commit is contained in:
1 parent
f397ad825c
commit
f94fc5bf07
1 file changed
+24
-6
@@ -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")) {
|
||||
|
||||
Reference in new issue
Block a user