Skip to main content
Glama
northernvariables

FedMCP - Federal Parliamentary Information

ChatMessage.tsx7.28 kB
/** * ChatMessage Component * * Renders individual chat messages with: * - Markdown formatting (react-markdown + remark-gfm) * - User vs. Assistant styling * - Timestamp display * - Copy to clipboard button */ 'use client'; import React from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Copy, Check, Share2 } from 'lucide-react'; import { MapleLeafIcon } from '@canadagpt/design-system'; import type { Message } from '@/lib/types/chat'; import { ResultsPromptCard } from './ResultsPromptCard'; interface ChatMessageProps { message: Message; } export function ChatMessage({ message }: ChatMessageProps) { const [copied, setCopied] = React.useState(false); const [shared, setShared] = React.useState(false); const [showResultsCard, setShowResultsCard] = React.useState(true); const handleCopy = async () => { await navigator.clipboard.writeText(message.content); setCopied(true); setTimeout(() => setCopied(false), 2000); }; const handleShare = async () => { const shareText = message.content; const shareUrl = window.location.href; // Check if Web Share API is available (mobile/modern browsers) if (navigator.share) { try { await navigator.share({ title: 'CanadaGPT Response', text: shareText, url: shareUrl, }); setShared(true); setTimeout(() => setShared(false), 2000); } catch (error) { // User cancelled or share failed console.log('Share cancelled or failed:', error); } } else { // Fallback: Copy link to clipboard const shareableText = `${shareText}\n\nShared from: ${shareUrl}`; await navigator.clipboard.writeText(shareableText); setShared(true); setTimeout(() => setShared(false), 2000); } }; const isUser = message.role === 'user'; const isAssistant = message.role === 'assistant'; return ( <div className={`group flex gap-3 ${ isUser ? 'flex-row-reverse' : 'flex-row' } mb-4`} > {/* Avatar */} <div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${ isUser ? 'bg-accent-red text-white' : 'bg-gray-700 text-white border border-gray-600' }`} > {isUser ? 'U' : <MapleLeafIcon className="w-5 h-5" size={20} />} </div> {/* Message Content */} <div className={`flex-1 max-w-[80%] ${ isUser ? 'items-end' : 'items-start' }`} > {/* Message Bubble */} <div className={`rounded-lg px-4 py-3 ${ isUser ? 'bg-accent-red text-white' : 'bg-gray-800 border border-gray-700 text-gray-100' }`} > {isAssistant ? ( <div className="text-sm text-gray-100"> <ReactMarkdown remarkPlugins={[remarkGfm]} components={{ // Custom link styling a: ({ node, ...props }) => ( <a {...props} className="text-accent-red hover:underline" target="_blank" rel="noopener noreferrer" /> ), // Custom code block styling code: ({ node, inline, ...props }: any) => inline ? ( <code {...props} className="bg-gray-700 text-gray-100 px-1.5 py-0.5 rounded text-sm font-mono" /> ) : ( <code {...props} className="block bg-gray-700 text-gray-100 p-3 rounded-md text-sm overflow-x-auto font-mono" /> ), // Custom paragraph spacing p: ({ node, ...props }) => ( <p {...props} className="mb-2 last:mb-0 text-gray-100" /> ), // Custom list styling ul: ({ node, ...props }) => ( <ul {...props} className="list-disc ml-4 mb-2 text-gray-100" /> ), ol: ({ node, ...props }) => ( <ol {...props} className="list-decimal ml-4 mb-2 text-gray-100" /> ), // Headers h1: ({ node, ...props }) => ( <h1 {...props} className="text-xl font-bold mb-2 text-gray-100" /> ), h2: ({ node, ...props }) => ( <h2 {...props} className="text-lg font-bold mb-2 text-gray-100" /> ), h3: ({ node, ...props }) => ( <h3 {...props} className="text-base font-bold mb-2 text-gray-100" /> ), }} > {message.content} </ReactMarkdown> </div> ) : ( <p className="text-sm whitespace-pre-wrap text-white">{message.content}</p> )} </div> {/* Results Prompt Card (if navigation provided by tool) */} {isAssistant && message.navigation && showResultsCard && ( <ResultsPromptCard url={message.navigation.url} onDismiss={() => setShowResultsCard(false)} /> )} {/* Message Metadata */} <div className={`flex items-center gap-2 mt-1 text-xs text-gray-500 ${ isUser ? 'flex-row-reverse' : 'flex-row' }`} > {/* Timestamp */} <span> {new Date(message.created_at).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', })} </span> {/* Token Count (for assistant messages) */} {isAssistant && message.tokens_total && ( <span className="text-gray-600"> {message.tokens_total.toLocaleString()} tokens </span> )} {/* Action Buttons (only visible on hover for assistant messages) */} {isAssistant && ( <> <button onClick={handleCopy} className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-gray-700 rounded" title="Copy message" > {copied ? ( <Check className="w-3.5 h-3.5 text-green-600" /> ) : ( <Copy className="w-3.5 h-3.5 text-gray-400" /> )} </button> <button onClick={handleShare} className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-gray-700 rounded" title="Share message" > {shared ? ( <Check className="w-3.5 h-3.5 text-green-600" /> ) : ( <Share2 className="w-3.5 h-3.5 text-gray-400" /> )} </button> </> )} </div> </div> </div> ); }

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/northernvariables/FedMCP'

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