trading-platform-frontend-v2/src/components/chat/ChatMessage.tsx
rckrdmrd 5b53c2539a feat: Initial commit - Trading Platform Frontend
React frontend with:
- Authentication UI
- Trading dashboard
- ML signals display
- Portfolio management

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 04:30:39 -06:00

183 lines
5.2 KiB
TypeScript

/**
* ChatMessage Component
* Renders an individual chat message with markdown support
*/
import React, { useState } from 'react';
import { Copy, Check, Bot, User } from 'lucide-react';
import type { ChatMessage as ChatMessageType } from '../../types/chat.types';
interface ChatMessageProps {
message: ChatMessageType;
}
export const ChatMessage: React.FC<ChatMessageProps> = ({ message }) => {
const [copied, setCopied] = useState(false);
const isUser = message.role === 'user';
const isAssistant = message.role === 'assistant';
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(message.content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
};
const formatTimestamp = (timestamp: string) => {
const date = new Date(timestamp);
const now = new Date();
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
if (minutes < 1) return 'Just now';
if (minutes < 60) return `${minutes}m ago`;
if (hours < 24) return `${hours}h ago`;
return date.toLocaleDateString();
};
// Simple markdown parser for basic formatting
const renderContent = (content: string) => {
// Split by code blocks first
const parts = content.split(/(```[\s\S]*?```|`[^`]+`)/g);
return parts.map((part, index) => {
// Code block
if (part.startsWith('```') && part.endsWith('```')) {
const code = part.slice(3, -3).trim();
const lines = code.split('\n');
const language = lines[0].match(/^\w+$/) ? lines.shift() : '';
return (
<pre key={index} className="bg-gray-950 rounded p-3 my-2 overflow-x-auto">
{language && (
<div className="text-xs text-gray-400 mb-2">{language}</div>
)}
<code className="text-sm text-gray-200">{lines.join('\n')}</code>
</pre>
);
}
// Inline code
if (part.startsWith('`') && part.endsWith('`')) {
return (
<code
key={index}
className="bg-gray-800 px-1.5 py-0.5 rounded text-sm text-gray-200"
>
{part.slice(1, -1)}
</code>
);
}
// Regular text with basic formatting
let formatted = part;
// Bold: **text** or __text__
formatted = formatted.replace(
/(\*\*|__)(.*?)\1/g,
'<strong class="font-semibold">$2</strong>'
);
// Italic: *text* or _text_
formatted = formatted.replace(
/([*_])(.*?)\1/g,
'<em class="italic">$2</em>'
);
// Bullet lists
formatted = formatted.replace(
/^[•\-*]\s+(.+)$/gm,
'<li class="ml-4">$1</li>'
);
// Numbered lists
formatted = formatted.replace(
/^\d+\.\s+(.+)$/gm,
'<li class="ml-4">$1</li>'
);
return (
<span
key={index}
dangerouslySetInnerHTML={{ __html: formatted }}
/>
);
});
};
return (
<div
className={`flex gap-3 py-4 px-4 group ${
isUser ? 'justify-end' : 'justify-start'
}`}
>
{/* Avatar */}
{!isUser && (
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
<Bot className="w-5 h-5 text-white" />
</div>
)}
{/* Message Content */}
<div className={`flex-1 max-w-[80%] ${isUser ? 'order-first' : ''}`}>
<div
className={`rounded-lg px-4 py-3 ${
isUser
? 'bg-blue-600 text-white ml-auto'
: 'bg-gray-800 text-gray-100'
}`}
>
{/* Message text */}
<div className="text-sm whitespace-pre-wrap break-words">
{renderContent(message.content)}
</div>
{/* Tools used badge */}
{isAssistant && message.toolsUsed && message.toolsUsed.length > 0 && (
<div className="mt-2 pt-2 border-t border-gray-700">
<div className="text-xs text-gray-400">
Tools: {message.toolsUsed.join(', ')}
</div>
</div>
)}
</div>
{/* Timestamp and actions */}
<div
className={`flex items-center gap-2 mt-1 text-xs text-gray-500 ${
isUser ? 'justify-end' : 'justify-start'
}`}
>
<span>{formatTimestamp(message.timestamp)}</span>
{/* Copy button */}
<button
onClick={handleCopy}
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:text-gray-300"
title="Copy message"
>
{copied ? (
<Check className="w-3 h-3 text-green-400" />
) : (
<Copy className="w-3 h-3" />
)}
</button>
</div>
</div>
{/* User Avatar */}
{isUser && (
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-gradient-to-br from-green-500 to-teal-600 flex items-center justify-center">
<User className="w-5 h-5 text-white" />
</div>
)}
</div>
);
};
export default ChatMessage;