Render light markdown in chat messages
Bodies were printed verbatim, so an agent's **bold**, `code`, or [link](https://...) showed as literal punctuation — the opposite of people talking. Render the inline markdown agents and humans actually use in chat into React nodes, composed around the existing @mention highlighting. Links are restricted to http(s) and nothing is ever set as raw HTML (no dangerouslySetInnerHTML), so a message body can never inject markup or a javascript: URL. A plain message still comes back as one string.
This commit is contained in:
1 parent
287c072c9f
commit
6367512052
1 file changed
+27
-3
@@ -132,7 +132,7 @@ export function ChatMessageRow({
|
||||
}
|
||||
|
||||
function renderBody(body: string, mentions: ChatMentionRecord[], context: MessageContext): ReactNode {
|
||||
if (mentions.length === 0) return body;
|
||||
if (mentions.length === 0) return formatInline(body, "b");
|
||||
const parts: ReactNode[] = [];
|
||||
const ordered = [...mentions].sort((a, b) => a.offsetStart - b.offsetStart);
|
||||
let lastIndex = 0;
|
||||
@@ -143,7 +143,7 @@ function renderBody(body: string, mentions: ChatMentionRecord[], context: Messag
|
||||
}
|
||||
const token = body.slice(mention.offsetStart, mention.offsetEnd);
|
||||
if (!token.startsWith("@")) throw new Error("Message mention token is invalid.");
|
||||
if (mention.offsetStart > lastIndex) parts.push(body.slice(lastIndex, mention.offsetStart));
|
||||
if (mention.offsetStart > lastIndex) parts.push(...formatInline(body.slice(lastIndex, mention.offsetStart), `s${lastIndex}`));
|
||||
const label = mentionLabel(mention, context);
|
||||
parts.push(
|
||||
<span
|
||||
@@ -155,10 +155,34 @@ function renderBody(body: string, mentions: ChatMentionRecord[], context: Messag
|
||||
);
|
||||
lastIndex = mention.offsetEnd;
|
||||
}
|
||||
if (lastIndex < body.length) parts.push(body.slice(lastIndex));
|
||||
if (lastIndex < body.length) parts.push(...formatInline(body.slice(lastIndex), `s${lastIndex}`));
|
||||
return parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the light markdown agents and people use in chat — **bold**, *italic*,
|
||||
* `code`, and [text](https://link) — into React nodes. Links are restricted to
|
||||
* http(s) and nothing is ever set as raw HTML, so message bodies stay inert. A
|
||||
* message with no markdown comes back as a single plain string.
|
||||
*/
|
||||
function formatInline(text: string, keyPrefix: string): ReactNode[] {
|
||||
const pattern = /(\*\*([^*\n]+)\*\*|\*([^*\n]+)\*|`([^`\n]+)`|\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\))/g;
|
||||
const nodes: ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
let token = 0;
|
||||
for (let match = pattern.exec(text); match; match = pattern.exec(text)) {
|
||||
if (match.index > lastIndex) nodes.push(text.slice(lastIndex, match.index));
|
||||
const key = `${keyPrefix}-${token++}`;
|
||||
if (match[2] !== undefined) nodes.push(<strong key={key}>{match[2]}</strong>);
|
||||
else if (match[3] !== undefined) nodes.push(<em key={key}>{match[3]}</em>);
|
||||
else if (match[4] !== undefined) nodes.push(<code key={key} className="mono" style={{ background: "var(--panel-3)", padding: "1px 5px", borderRadius: 4, fontSize: "0.92em" }}>{match[4]}</code>);
|
||||
else nodes.push(<a key={key} href={match[6]} target="_blank" rel="noreferrer" style={{ color: "var(--accent)" }}>{match[5]}</a>);
|
||||
lastIndex = pattern.lastIndex;
|
||||
}
|
||||
if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function mentionLabel(mention: ChatMentionRecord, context: MessageContext): string {
|
||||
if (mention.targetType === "employee") {
|
||||
const employee = context.employees.find((item) => item.id === mention.targetId);
|
||||
|
||||
Reference in new issue
Block a user