Spaces:
Running
Running
File size: 3,946 Bytes
c3a0082 e2487ef 0cc5af3 e2487ef 0cc5af3 c3a0082 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | import React from 'react';
import CodeBlock from './CodeBlock';
const formatText = (text) => {
// β
Handle base64 images
const imageRegex = /\[IMAGE_START\](.*?)\[IMAGE_END\]/gs;
text = text.replace(imageRegex, (match, base64) => {
const src = `data:image/png;base64,${base64.trim()}`;
return `<img src="${src}" alt="Generated Image" class="chat-image"/>`;
});
// β
Normalize line endings and remove excessive blank lines
text = text.replace(/\r\n|\r/g, '\n');
text = text.replace(/\n{3,}/g, '\n\n');
// β
Parse fenced code blocks (```code```)
text = text.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
const language = lang ? ` class="language-${lang}"` : '';
return `<pre><code${language}>${code.trim().replace(/</g, '<').replace(/>/g, '>')}</code></pre>`;
});
// β
Parse blockquotes
text = text.replace(/^> (.*)$/gm, '<blockquote>$1</blockquote>');
// β
Headings
text = text.replace(/^### (.*)$/gm, '<h3>$1</h3>');
// β
Horizontal rules
text = text.replace(/^---$/gm, '<hr>');
// β
Bold (**text**) and italic (*text*)
text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
text = text.replace(/\*(.*?)\*/g, '<em>$1</em>');
// β
Emoji rendering using colon syntax (:smile:)
const emojiMap = {
smile: "π",
sad: "π’",
heart: "β€οΈ",
thumbs_up: "π",
fire: "π₯",
check: "β
",
x: "β",
star: "β",
rocket: "π",
warning: "β οΈ",
};
text = text.replace(/:([a-z0-9_+-]+):/g, (match, name) => emojiMap[name] || match);
// β
Unordered list (bullets)
const listify = (lines, tag) =>
`<${tag}>` +
lines.map(item => `<li>${item.replace(/^(\-|\d+\.)\s*/, '').trim()}</li>`).join('') +
`</${tag}>`;
text = text.replace(
/((?:^[-*] .+(?:\n|$))+)/gm,
(match) => listify(match.trim().split('\n'), 'ul')
);
// β
Ordered list (fix separate `1.` items issue)
text = text.replace(/^(\d+\. .+)$/gm, '__ORDERED__START__$1__ORDERED__END__');
text = text.replace(
/__ORDERED__START__(\d+\. .+?)__ORDERED__END__/gs,
(_, line) => `<ol><li>${line.replace(/^\d+\.\s*/, '')}</li></ol>`
);
text = text.replace(/<\/ol>\s*<ol>/g, '');
// β
Markdown-style tables
text = text.replace(
/^\|(.+?)\|\n\|([-:| ]+)\|\n((?:\|.*\|\n?)*)/gm,
(_, headerRow, dividerRow, bodyRows) => {
const headers = headerRow.split('|').map(h => `<th>${h.trim()}</th>`).join('');
const rows = bodyRows.trim().split('\n').map(r =>
'<tr>' + r.split('|').map(cell => `<td>${cell.trim()}</td>`).join('') + '</tr>'
).join('');
return `<table><thead><tr>${headers}</tr></thead><tbody>${rows}</tbody></table>`;
}
);
// β
Paragraphs and line breaks inside paragraphs
const blocks = text.split(/\n{2,}/).map(block => {
if (
block.startsWith('<h3>') ||
block.startsWith('<hr>') ||
block.startsWith('<ul>') ||
block.startsWith('<ol>') ||
block.startsWith('<table>') ||
block.startsWith('<pre>') ||
block.startsWith('<blockquote>') ||
block.startsWith('<img')
) {
return block;
} else {
return `<p>${block.trim().replace(/\n/g, '<br>')}</p>`;
}
});
return blocks.join('\n');
};
export default function MessageBubble({ role, content, time }) {
return (
<div className={`message ${role}`}>
<div className="bubble">
<FormattedContent content={content} />
<div className="timestamp">{time}</div>
</div>
</div>
);
}
function FormattedContent({ content }) {
const blocks = content.split('```');
return (
<>
{blocks.map((block, i) =>
i % 2 === 1 ? (
<CodeBlock key={i} content={block} />
) : (
<div
key={i}
className="formatted-text"
dangerouslySetInnerHTML={{ __html: formatText(block) }}
/>
)
)}
</>
);
}
|