DevMate / src /components /ChatBox.jsx
FrederickSundeep's picture
commit 02116
cff0a04
raw
history blame
4.33 kB
import React from "react";
import CodeBlock from "./CodeBlock";
import "./ChatBox.css"; // Add dot animation CSS
export default function ChatBox({ messages, loading }) {
return (
<div className="chat-box">
{messages.map((msg, i) => (
msg.content?.trim() && ( <div key={i} className={`message ${msg.role}`}>
<div className="bubble">
<Message content={msg.content} />
</div>
</div>
)
))}
{/* βœ… Typing indicator when loading */}
{loading && (
<div className="message assistant">
<div className="typing-indicator">
<span className="dot"></span>
<span className="dot"></span>
<span className="dot"></span>
</div>
</div>
)}
</div>
);
}
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, '&lt;').replace(/>/g, '&gt;')}</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');
};
function Message({ content }) {
const parts = content.split(/```(?:[a-z]*)\n([\s\S]*?)```/g);
return (
<>
{parts.map((part, i) =>
i % 2 === 1 ? <CodeBlock key={i} code={part.trim()} /> : <div
key={i}
className="formatted-text"
dangerouslySetInnerHTML={{ __html: formatText(part) }}
/>
)}
</>
);
}