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

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