Files
homarr/apps/nextjs/src/components/assistant/assistant-markdown.ts
T
2026-07-30 16:56:43 +02:00

42 lines
1.5 KiB
TypeScript

const protectedMarkdownPattern = /```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)|`+[^`\n]*`+/gu;
const inlineStrongListMarker = / - (?=\*\*[^*\n]{1,80}\*\*\s+(?:[—–:-]\s+)?)/gu;
const trailingQuestion = /\s+((?:Are|Can|Could|Do|Does|How|Is|What|When|Where|Which|Why|Would)\b[^?\n]*\?)$/u;
const normalizeProse = (text: string) => {
const withLineBreaks = text.replaceAll("\\r\\n", "\n").replaceAll("\\n", "\n");
return withLineBreaks
.split("\n")
.map((line) => {
const markers = [...line.matchAll(inlineStrongListMarker)];
if (markers.length < 2) return line;
let markerIndex = 0;
return line
.replaceAll(inlineStrongListMarker, () => (markerIndex++ === 0 ? "\n\n- " : "\n- "))
.replace(trailingQuestion, "\n\n$1");
})
.join("\n");
};
/**
* Makes common streamed model Markdown readable without changing code samples.
*
* Models occasionally emit escaped line breaks or an entire bold-label list on
* one line. The assistant-ui renderer receives this function through its
* streaming-safe `preprocess` hook, before Markdown is parsed.
*/
export const normalizeAssistantMarkdown = (text: string) => {
let offset = 0;
let normalized = "";
for (const match of text.matchAll(protectedMarkdownPattern)) {
const index = match.index;
normalized += normalizeProse(text.slice(offset, index));
normalized += match[0];
offset = index + match[0].length;
}
return normalized + normalizeProse(text.slice(offset));
};