/**
* 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 (
{code}
{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) } // ── 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(cleaned)}
})}
| {parseInlineMarkdown(h)} | )}
|---|
| {parseInlineMarkdown(cell)} | )}
| {parseInlineMarkdown(h)} | )}
|---|
| {parseInlineMarkdown(cell)} | )}
{parseInlineMarkdown(block)}
) }) }