// ================================================================================== // MARKDOWN TEXT COMPONENT // Renders markdown text with proper formatting // ================================================================================== const MarkdownText = ({ text, isUserMessage }) => { const renderMarkdown = (text) => { if (!text) { return ''; } // Collapse 3+ newlines to 2 so AI over-spacing doesn't create huge gaps text = text.replace(/\n{3,}/g, '\n\n'); // Fix common "numbered list got squished into one line" cases (tickets) if (text.includes('Ticket ID:')) { text = text.replace(/(? { const token = `MDLINKTOKEN${linkTokens.length}`; linkTokens.push({ label, url }); return token; }); // 2) Tokenize HTML anchors if the model ever outputs them text = text.replace(/]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi, (match, url, label) => { const token = `MDLINKTOKEN${linkTokens.length}`; linkTokens.push({ label, url }); return token; }); // 3) Tokenize bare URLs so underscores don't get parsed as italics text = text.replace(/(https?:\/\/[^\s<>"']+)/g, (match, url) => { const token = `MDURLTOKEN${urlTokens.length}`; urlTokens.push({ url }); return token; }); // 4) Escape any raw HTML const escapeHtml = (s) => s .replace(/&/g, '&') .replace(//g, '>'); let safe = escapeHtml(text); // Convert markdown to HTML (order matters! match #### before ###) let html = safe // Headers — mark them so we don't insert extra
around them later .replace(/^#### (.+)$/gm, '

$1

') .replace(/^### (.+)$/gm, '

$1

') .replace(/^## (.+)$/gm, '

$1

') .replace(/^# (.+)$/gm, '

$1

') // Bold .replace(/\*\*([^\*]+?)\*\*/g, '$1') .replace(/__([^_]+?)__/g, '$1') // Italic .replace(/(?$1') .replace(/(?$1') // Code .replace(/`([^`]+?)`/g, '$1'); // Restore markdown link tokens html = html.replace(/(?:__)?MDLINKTOKEN(\d+)(?:__)?/g, (match, idx) => { const i = parseInt(idx, 10); const item = linkTokens[i]; if (!item) return match; return `${item.label}`; }); // Restore bare URL tokens html = html.replace(/(?:__)?MDURLTOKEN(\d+)(?:__)?/g, (match, idx) => { const i = parseInt(idx, 10); const item = urlTokens[i]; if (!item) return match; return `${item.url}`; }); // Process lists const lines = html.split('\n'); let inUl = false; let inOl = false; let processedLines = []; const closeLists = () => { // Close any open lists. We deliberately do NOT tag these with // so that a real line break between a bullet block and the next vendor header // becomes a small visual gap between groups. if (inUl) { processedLines.push(''); inUl = false; } if (inOl) { processedLines.push(''); inOl = false; } }; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Allow a bit of leading whitespace before bullets so lines like " - Phone: ..." // are still treated as list items. const ulMatch = line.match(/^\s*[\*\-]\s+(.+)$/); const olMatch = line.match(/^\s*(\d+)\.\s+(.+)$/); if (ulMatch) { if (inOl) { processedLines.push(''); inOl = false; } if (!inUl) { // Start a new bullet list with minimal extra spacing. // Tag with so we can strip the automatic
after block tags later. processedLines.push(''); inUl = false; } if (!inOl) { // Start a new ordered list with minimal extra spacing processedLines.push('
    '); inOl = true; } processedLines.push(`
  1. ${olMatch[2]}
  2. `); continue; } closeLists(); if (line.trim()) { processedLines.push(line); } } closeLists(); // Join lines and convert line breaks. // 1) Join using newlines so we can reason about where
    should appear. // 2) Strip newlines that immediately follow special markers attached // to block elements like headings and list tags, so we don't get stray
    // between
      ,
        ,
      • , and headings. // 3) Collapse any remaining multiple newlines, then convert to
        . html = processedLines.join('\n'); html = html.replace(/\n/g, ''); // remove newline right after marker html = html.replace(/\n{2,}/g, '\n'); html = html.replace(/\n/g, '
        '); html = html.replace(//g, ''); // finally drop the markers // Final cleanup: remove any stray
        between vendor headers (in
          ) // and the bullet list that follows, so the bullets "hug" the header. html = html.replace(/<\/ol>\s*
            ); }; if (typeof window !== 'undefined') window.MarkdownText = MarkdownText;