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, '&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) }}
          />
        )
      )}
    </>
  );
}