DevMate / src /components /ChatBox.jsx
FrederickSundeep's picture
code updated to fix the scroll to bottom issue resolved
01bbc8b
import React, { useRef, useEffect } from 'react';
import CodeBlock from "./CodeBlock";
import { Box, Paper, CircularProgress, Typography } from "@mui/material";
import "./ChatBox.css"; // Add dot animation CSS
export default function ChatBox({ messages, loading }) {
const bottomRef = useRef(null);
useEffect(() => {
if (!bottomRef.current) return;
if (loading) {
bottomRef.current.scrollIntoView({ behavior: 'auto' });
} else {
// Smooth scroll after streaming ends with slight delay
setTimeout(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, 80); // tweak timing based on UX
}
}, [messages, loading]);
return (
<Box className="chat-box">
{messages.map((msg, i) =>
msg.content?.trim() ? (
<Box
key={i}
className={`message ${msg.role}`}
// sx={{
// display: "flex",
// justifyContent: msg.role === "user" ? "flex-end" : "flex-start",
// mb: 1.5,
// }}
>
<Paper
elevation={3}
sx={{
p: 1.5,
bgcolor: msg.role === "user" ? "#1e1e2f" : "#2c2c3e",
color: "#fff",
maxWidth: "100%",
borderRadius: 2,
// overflowX: "auto",
}}
>
<div className="bubble">
<Message content={msg.content} />
</div>
</Paper>
</Box>
) : null
)}
{loading && (
<Box
className="message assistant"
sx={{
display: "flex",
justifyContent: "flex-start",
pl: 1,
mt: 1,
}}
>
<Box className="typing-indicator">
<span className="dot" />
<span className="dot" />
<span className="dot" />
</Box>
</Box>
)}
<Box ref={bottomRef} />
</Box>
);
}
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) }}
/>
)
)}
</>
);
}