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