NLP-IBM-Debater / src /app /components /chat /MessageList.tsx
S01Nour
feat/ui: chat adapted tools
a94de35
raw
history blame
7.56 kB
import React, { useEffect, useRef } from 'react';
import type { ChatMessage } from '../../types/chat.types.ts';
import { Loader2, AlertCircle, RotateCcw } from 'lucide-react';
import AudioPlayer from './AudioPlayer.tsx';
type MessageListProps = {
messages: ChatMessage[];
isLoading?: boolean;
error?: string | null;
onRetry?: () => void;
};
const MessageList = ({ messages, isLoading = false, error = null, onRetry }: MessageListProps) => {
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages, isLoading]);
const formatTime = (date: Date) => {
return date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
});
};
const getToolClasses = (message: ChatMessage) => {
if (message.role !== 'assistant' || !message.tool) return '';
const toolLower = message.tool.toLowerCase();
// Match tool names flexibly (case-insensitive, handles variations)
if (toolLower.includes('detect') && toolLower.includes('stance')) {
return 'border-l-4 border-blue-400 pl-3 bg-blue-50/30 dark:bg-blue-900/10';
}
if (toolLower.includes('generate') && toolLower.includes('argument')) {
return 'border-l-4 border-purple-400 pl-3 bg-purple-50/30 dark:bg-purple-900/10';
}
if (toolLower.includes('extract') && toolLower.includes('topic')) {
return 'border-l-4 border-emerald-400 pl-3 bg-emerald-50/30 dark:bg-emerald-900/10';
}
// Default styling for other tools
return 'border-l-4 border-teal-400 pl-3 bg-teal-50/30 dark:bg-teal-900/10';
};
const getToolLabel = (tool: string | null | undefined): string => {
if (!tool) return '';
const toolLower = tool.toLowerCase();
if (toolLower.includes('detect') && toolLower.includes('stance')) {
return 'Detect Stance';
}
if (toolLower.includes('generate') && toolLower.includes('argument')) {
return 'Generate Argument';
}
if (toolLower.includes('extract') && toolLower.includes('topic')) {
return 'Extract Topic';
}
// Return formatted tool name (capitalize first letter of each word)
return tool.split(/[\s_-]+/).map(word =>
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
).join(' ');
};
return (
<div className="flex-1 overflow-y-auto px-4 py-6 space-y-4">
{messages.length === 0 && !isLoading && (
<div className="flex flex-col items-center justify-center h-full text-center">
<div className="mb-4">
<div className="w-16 h-16 bg-gradient-to-br from-teal-400 to-blue-500 rounded-full flex items-center justify-center">
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
</svg>
</div>
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
Start a conversation
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 max-w-md">
Ask me anything! I'm powered by Meta's Llama 4 Scout model and can help with a wide range of topics.
</p>
</div>
)}
{messages.map((message) => {
return(
<div
key={message.id}
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-3xl px-4 py-3 rounded-2xl ${
message.role === 'user'
? 'bg-teal-500 text-white ml-12'
: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 mr-12'
}${getToolClasses(message)}`} // Apply tool-specific classes
>
{message.audioUrl ? (
// Audio message display
<div>
<AudioPlayer src={message.audioUrl} />
</div>
) : (
// Regular text message
<div>
{message.tool && message.role === 'assistant' && (
<div className="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{getToolLabel(message.tool)}
</div>
)}
<div className="whitespace-pre-wrap break-words">
{message.content}
</div>
{message.role === 'assistant' && message.tool &&
message.tool.toLowerCase().includes('generate') &&
message.tool.toLowerCase().includes('argument') && (
<div className="mt-3 flex gap-2">
<button
type="button"
className="px-3 py-1 text-xs rounded-full bg-emerald-100 text-emerald-800 hover:bg-emerald-200 dark:bg-emerald-900/40 dark:text-emerald-200 transition-colors"
>
Positive stance
</button>
<button
type="button"
className="px-3 py-1 text-xs rounded-full bg-rose-100 text-rose-800 hover:bg-rose-200 dark:bg-rose-900/40 dark:text-rose-200 transition-colors"
>
Negative stance
</button>
</div>
)}
</div>
)}
<div
className={`text-xs mt-1 ${
message.role === 'user'
? 'text-teal-100'
: 'text-gray-500 dark:text-gray-400'
}`}
>
{formatTime(message.timestamp)}
</div>
</div>
</div>
)})}
{isLoading && (
<div className="flex justify-start">
<div className="bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 mr-12 max-w-3xl px-4 py-3 rounded-2xl">
<div className="flex items-center space-x-2">
<Loader2 className="w-4 h-4 animate-spin" />
<span className="text-sm">Thinking...</span>
</div>
</div>
</div>
)}
{error && (
<div className="flex justify-start">
<div className="bg-red-50 dark:bg-red-900/20 text-red-900 dark:text-red-100 mr-12 max-w-3xl px-4 py-3 rounded-2xl border border-red-200 dark:border-red-800">
<div className="flex items-start space-x-2">
<AlertCircle className="w-5 h-5 text-red-500 dark:text-red-400 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<p className="text-sm font-medium">Error</p>
<p className="text-sm opacity-90">{error}</p>
{onRetry && (
<button
onClick={onRetry}
className="mt-2 flex items-center space-x-1 text-sm text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 transition-colors"
>
<RotateCcw className="w-3 h-3" />
<span>Retry</span>
</button>
)}
</div>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
);
};
export default MessageList;