/** * Convert URLs in text to clickable tags. */ export function linkify(text) { const regex = /https?:\/\/[^\s]+|www\.[^\s]+/g const tokens = [] let lastIndex = 0 for (const match of text.matchAll(regex)) { const start = match.index ?? 0 if (start > lastIndex) { tokens.push(text.slice(lastIndex, start)) } const rawUrl = match[0] const href = rawUrl.startsWith('http') ? rawUrl : `https://${rawUrl}` tokens.push( {rawUrl} , ) lastIndex = start + rawUrl.length } if (lastIndex < text.length) { tokens.push(text.slice(lastIndex)) } return tokens.length === 0 ? text : tokens } function parseInlineMarkdown(str) { const parts = [] let lastIndex = 0 // Match bold+italic, bold, italic, inline code, and links const regex = /\*\*\*([^*]+)\*\*\*|\*\*([^*]+)\*\*|\*([^*]+)\*|_([^_]+)_|`([^`]+)`|https?:\/\/[^\s]+|www\.[^\s]+/g for (const match of str.matchAll(regex)) { const start = match.index if (start > lastIndex) { parts.push(str.slice(lastIndex, start)) } if (match[1]) { parts.push({match[1]}) } else if (match[2]) { parts.push({match[2]}) } else if (match[3]) { parts.push({match[3]}) } else if (match[4]) { parts.push({match[4]}) } else if (match[5]) { parts.push({match[5]}) } else { const url = match[0] const href = url.startsWith('http') ? url : `https://${url}` parts.push( {url} , ) } lastIndex = start + match[0].length } if (lastIndex < str.length) { parts.push(str.slice(lastIndex)) } return parts.length === 0 ? str : parts } /** * Format assistant message text with rich formatting. * Supports: headings (##, ###), bold, italic, inline code, code blocks, * bullet lists, numbered lists, blockquotes, tables, and links. */ export function formatMarkdown(text) { // Remove
and
tags text = text.replace(//gi, '\n') // Split into blocks (paragraphs, lists, headings, etc) const blocks = text.split(/\n{2,}/).map(block => block.trim()).filter(b => b) return blocks.map((block, idx) => { // ── Code block (```...``` or ````...````) ────────────────────────── if (block.startsWith('```') || block.startsWith('````')) { const match = block.match(/^`{3,4}(\w*)\n?([\s\S]*?)`{3,4}$/) if (match) { const lang = match[1] || '' const code = match[2].trim() return (
{lang &&
{lang}
}
{code}
) } // Fallback: render as pre return (
          {block.replace(/`{3,4}/g, '').trim()}
        
) } // ── Blockquote (lines starting with >) ──────────────────────────── if (block.startsWith('>')) { const quoteLines = block.split('\n').filter(l => l.trim()) return (
{quoteLines.map((line, i) => { const cleaned = line.replace(/^>\s?/, '') return

{parseInlineMarkdown(cleaned)}

})}
) } // ── Heading (### or ##) ─────────────────────────────────────────── const headingMatch = block.match(/^(#{1,3})\s+(.+)/) if (headingMatch) { const level = headingMatch[1].length const text = headingMatch[2] const Tag = level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3' return ( {parseInlineMarkdown(text)} ) } // ── Bullet list (lines starting with •, -, or *) ────────────────── if (block.includes('•') || block.match(/^\s*[-*]\s/m)) { const items = block.split('\n').filter(l => l.trim()) return ( ) } // ── Numbered list (lines starting with 1., 2., etc) ─────────────── if (block.match(/^\d+\.\s/m)) { const items = block.split('\n').filter(l => l.trim()) return (
    {items.map((item, i) => { const cleaned = item.replace(/^\d+\.\s*/, '') return
  1. {parseInlineMarkdown(cleaned)}
  2. })}
) } // ── Table-like structure (contains | and ---) ───────────────────── if (block.includes('|') && block.includes('---')) { const lines = block.split('\n').filter(l => l.trim()) const headerRow = lines[0].split('|').map(c => c.trim()).filter(Boolean) // Skip separator line (---|---) const dataRows = lines.slice(2).map(line => line.split('|').map(c => c.trim()).filter(Boolean) ) if (headerRow.length > 0 && dataRows.length > 0) { return (
{headerRow.map((h, i) => )} {dataRows.map((row, ri) => ( {row.map((cell, ci) => )} ))}
{parseInlineMarkdown(h)}
{parseInlineMarkdown(cell)}
) } // Fallback table rendering (no separator line) } if (block.includes('|')) { const lines = block.split('\n').filter(l => l.trim()) const rows = lines.map(line => line.split('|').map(c => c.trim()).filter(Boolean) ) if (rows.length > 1 && rows[0].length > 1) { const header = rows[0] const data = rows.slice(1).filter(r => r.length === header.length) return (
{header.map((h, i) => )} {data.map((row, ri) => ( {row.map((cell, ci) => )} ))}
{parseInlineMarkdown(h)}
{parseInlineMarkdown(cell)}
) } } // ── Regular paragraph ───────────────────────────────────────────── return (

{parseInlineMarkdown(block)}

) }) }