Skip to main content
Glama

Multi-Agent Communication Platform (MCP)

RichMessageItem.tsx6.16 kB
import React, { useEffect, useRef, useState } from 'react'; import { marked } from 'marked'; import DOMPurify from 'dompurify'; import hljs from 'highlight.js'; import 'highlight.js/styles/github.css'; import { Message } from '../types'; import './RichMessageItem.css'; interface Props { message: Message; } // Configure marked options marked.setOptions({ gfm: true, breaks: true, pedantic: false }); // Custom renderer for better formatting const renderer = new marked.Renderer(); // Enhanced link rendering renderer.link = (options: any) => { const { href, title, tokens } = options; const text = tokens?.[0]?.text || ''; const isExternal = href?.startsWith('http'); return `<a href="${href}" ${title ? `title="${title}"` : ''} class="rich-link" ${isExternal ? 'target="_blank" rel="noopener noreferrer"' : ''}> ${text}${isExternal ? ' 🔗' : ''} </a>`; }; // Enhanced code blocks with copy button renderer.code = (options: any) => { const { text: code, lang: language } = options; const validLang = language && hljs.getLanguage(language) ? language : 'plaintext'; let highlighted; try { if (language && hljs.getLanguage(language)) { highlighted = hljs.highlight(code, { language: validLang }).value; } else { highlighted = hljs.highlightAuto(code).value; } } catch (err) { highlighted = code; // Fallback to plain text } const codeId = `code-${Math.random().toString(36).substr(2, 9)}`; return ` <div class="rich-code-block"> <div class="code-header"> <span class="code-language">${validLang}</span> <button class="copy-btn" data-code="${encodeURIComponent(code)}" data-id="${codeId}"> <span class="copy-text">Copy</span> </button> </div> <pre><code class="hljs language-${validLang}">${highlighted}</code></pre> </div> `; }; // Task list support renderer.listitem = (options: any) => { const { text } = options; const isTaskList = text.includes('type="checkbox"'); return `<li${isTaskList ? ' class="task-item"' : ''}>${text}</li>`; }; // Better blockquotes renderer.blockquote = (quote) => { return `<blockquote class="rich-quote">${quote}</blockquote>`; }; // Tables with better styling renderer.table = (options: any) => { const { header, rows } = options; return ` <div class="table-wrapper"> <table class="rich-table"> ${header} ${rows} </table> </div> `; }; marked.use({ renderer }); const RichMessageItem: React.FC<Props> = ({ message }) => { const messageRef = useRef<HTMLDivElement>(null); const [copiedId, setCopiedId] = useState<string | null>(null); const formatTimestamp = (timestamp: string) => { const date = new Date(timestamp); const now = new Date(); const diff = now.getTime() - date.getTime(); // Show relative time for recent messages if (diff < 60000) return 'just now'; if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`; return date.toLocaleTimeString(); }; // Process content for mentions and emojis const processContent = (content: string) => { // Handle mentions let processed = content.replace(/@(\w+)/g, '<span class="mention">@$1</span>'); // Add emoji support processed = processed .replace(/:\)/g, '😊') .replace(/:\(/g, '😔') .replace(/:D/g, '😃') .replace(/:P/g, '😛') .replace(/:\|/g, '😐') .replace(/<3/g, '❤️') .replace(/:\+1:/g, '👍') .replace(/:-1:/g, '👎'); return processed; }; useEffect(() => { // Add click handlers for copy buttons const copyButtons = messageRef.current?.querySelectorAll('.copy-btn'); const handleCopy = async (e: Event) => { const button = e.currentTarget as HTMLButtonElement; const code = decodeURIComponent(button.dataset.code || ''); const id = button.dataset.id || ''; try { await navigator.clipboard.writeText(code); setCopiedId(id); setTimeout(() => setCopiedId(null), 2000); } catch (err) { console.error('Failed to copy:', err); } }; copyButtons?.forEach(btn => { btn.addEventListener('click', handleCopy); }); return () => { copyButtons?.forEach(btn => { btn.removeEventListener('click', handleCopy); }); }; }, [message.content]); // Render markdown and sanitize const renderContent = () => { const processed = processContent(message.content); const html = marked(processed) as string; const sanitized = DOMPurify.sanitize(html, { ADD_ATTR: ['target', 'rel', 'data-code', 'data-id'], ADD_TAGS: ['button'], }); return { __html: sanitized }; }; return ( <div className="rich-message-item" ref={messageRef}> <div className="message-header"> <div className="sender-info"> <div className="sender-avatar"> {message.sender.username.charAt(0).toUpperCase()} </div> <span className="sender-name">@{message.sender.username}</span> <span className="sender-role">Agent</span> </div> <span className="timestamp" title={new Date(message.timestamp).toLocaleString()}> {formatTimestamp(message.timestamp)} </span> </div> <div className="rich-content" dangerouslySetInnerHTML={renderContent()} /> {message.read_by.length > 0 && ( <div className="read-status"> <span className="read-indicator" title={ message.read_by.map(r => `@${r.username}`).join(', ') }> ✓✓ Read by {message.read_by.length} </span> </div> )} {/* Update copy button text dynamically */} <style>{` .copy-btn[data-id="${copiedId}"] .copy-text::after { content: "✓ Copied!"; } .copy-btn[data-id="${copiedId}"] .copy-text { color: #10b981; } `}</style> </div> ); }; export default RichMessageItem;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/thiagovictorino/chat-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server