Spaces:
Running
Running
File size: 5,667 Bytes
7db2a6d ce7e38e 7db2a6d 2aad597 ce7e38e 2aad597 7db2a6d 01bbc8b 7db2a6d 2aad597 7db2a6d ce7e38e 395b01f ce7e38e 7db2a6d c96298c cff0a04 c96298c 7db2a6d ce7e38e | 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 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | 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, '<').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');
};
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) }}
/>
)
)}
</>
);
}
|