const closeTag = `${typeCandidate}>`;
const contentStart = typeEnd;
// Find the closing tag
const tagClose = text.indexOf(closeTag, cursor);
if (tagClose >= 0) {
const name = text.slice(contentStart, tagClose);
const fullMatch = text.slice(next, tagClose + closeTag.length);
tokens.push({ full: fullMatch, type: typeCandidate as MentionType, name });
cursor = tagClose + closeTag.length;
continue;
}
}
}
cursor = next + 1;
}
return tokens;
}
function extractFirstMentionName(text: string, type: MentionType): string | null {
const token = extractMentionTokens(text).find((item) => item.type === type);
return token?.name ?? null;
}
interface MessageContentWithMentionsProps {
content: string;
}
/** Renders message content with @mention highlighting using web components */
export const MessageContentWithMentions = memo(function MessageContentWithMentions({
content,
}: MessageContentWithMentionsProps) {
// Register web components on first render
useEffect(() => {
registerMentionComponents();
}, []);
const processed = useMemo(() => {
const tokens = extractMentionTokens(content);
if (tokens.length === 0) return [{ type: 'text' as const, content }];
const parts: Array<{ type: 'text'; content: string } | { type: 'mention'; mention: MentionToken }> = [];
let cursor = 0;
for (const token of tokens) {
const idx = content.indexOf(token.full, cursor);
if (idx === -1) continue;
if (idx > cursor) {
parts.push({ type: 'text', content: content.slice(cursor, idx) });
}
parts.push({ type: 'mention', mention: token });
cursor = idx + token.full.length;
}
if (cursor < content.length) {
parts.push({ type: 'text', content: content.slice(cursor) });
}
return parts;
}, [content]);
return (
{processed.map((part, i) =>
part.type === 'mention' ? (
part.mention.type === 'user' ? (
// @ts-ignore custom element
) : part.mention.type === 'repository' ? (
// @ts-ignore custom element
) : part.mention.type === 'ai' ? (
// @ts-ignore custom element
) : (
@{part.mention.name}
)
) : (
{part.content}
),
)}
);
});
/** Extract first mentioned user name from text */
export function extractMentionedUserUid(
text: string,
participants: Array<{ uid: string; name: string; is_ai: boolean }>,
): string | null {
const userName = extractFirstMentionName(text, 'user');
if (!userName) return null;
const user = participants.find((p) => !p.is_ai && p.name === userName);
return user ? user.uid : null;
}
/** Extract first mentioned AI name from text */
export function extractMentionedAiUid(
text: string,
participants: Array<{ uid: string; name: string; is_ai: boolean }>,
): string | null {
const aiName = extractFirstMentionName(text, 'ai');
if (!aiName) return null;
const ai = participants.find((p) => p.is_ai && p.name === aiName);
return ai ? ai.uid : null;
}