import React, { useEffect, useRef } from 'react'; import ReactMarkdown from 'react-markdown'; import type { ChatMessage } from '../../types/chat.types.ts'; import { Loader2, AlertCircle, RotateCcw } from 'lucide-react'; import AudioPlayer from './AudioPlayer.tsx'; import TranscriptToggle from './TranscriptToggle.tsx'; type MessageListProps = { messages: ChatMessage[]; isLoading?: boolean; error?: string | null; onRetry?: () => void; }; /** * Helper to determine if a message should use the specialized detection stance UI */ const isSpecializedStanceUI = (message: ChatMessage): boolean => { const toolLower = message.tool?.toLowerCase() || ''; if (!toolLower.includes('detect') || !toolLower.includes('stance')) return false; if (message.role === 'user') { return message.content.includes('**Topic:**'); } return message.role === 'assistant'; }; const MessageList = ({ messages, isLoading = false, error = null, onRetry }: MessageListProps) => { const messagesEndRef = useRef(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 (
{messages.length === 0 && !isLoading && (

Start a conversation

Ask me anything! I'm powered by Meta's Llama 4 Scout model and can help with a wide range of topics.

)} {messages.map((message, index) => { const showSpecializedStance = isSpecializedStanceUI(message); return (
{message.audioUrl ? (
) : (
{message.tool && message.role === 'assistant' && (
{getToolLabel(message.tool)}
)} {showSpecializedStance ? (
{message.role === 'user' ? (
{message.content.split('\n').map((line, idx) => { if (line.startsWith('**Topic:**')) { const topicText = line.replace('**Topic:**', '').trim(); return (
Topic: {topicText}
); } else if (line.startsWith('**Argument:**')) { const argumentText = line.replace('**Argument:**', '').trim(); return (
Argument: {argumentText}
); } return null; })}
) : (
{message.content.split('\n').map((line, idx) => { if (line.startsWith('**Stance:**')) { const stanceText = line.replace('**Stance:**', '').trim(); const isPro = stanceText.includes('PRO') || stanceText.toLowerCase().includes('positive'); return (
Stance: {stanceText}
); } else if (line.startsWith('**Confidence:**')) { const confidenceText = line.replace('**Confidence:**', '').trim(); const confidenceValue = parseFloat(confidenceText.replace('%', '')) || 0; const getConfidenceColor = (value: number) => { if (value >= 80) return 'bg-emerald-500'; if (value >= 60) return 'bg-yellow-500'; return 'bg-orange-500'; }; return (
Confidence: {confidenceText}
); } else if (line.startsWith('**Explanation:**')) { const explanationParts = message.content.split('**Explanation:**\n'); const explanationText = explanationParts.length > 1 ? explanationParts[1] : ''; return (
Explanation:
{explanationText}
); } return null; })}
)}
) : (
(

), ul: ({ node, ...props }) => (

    ), ol: ({ node, ...props }) => (
      ), li: ({ node, ...props }) => (
    1. ), }} > {typeof message.content === 'string' ? message.content : ''}
)} {/* Process pipeline for extract topic tool */} {message.role === 'assistant' && message.tool?.toLowerCase().includes('extract') && message.tool?.toLowerCase().includes('topic') && message.process && (
Process: {message.process.split('→').map((step, sIdx, steps) => ( {step.trim()} {sIdx < steps.length - 1 && ( )} ))}
)} {/* Stance info for generate argument tool */} {message.role === 'assistant' && message.tool?.toLowerCase().includes('generate') && message.tool?.toLowerCase().includes('argument') && message.stance && (
{message.stance} stance
)}
)}
{formatTime(message.timestamp)}
); })} {isLoading && (
Thinking...
)} {error && (

Error

{error}

{onRetry && ( )}
)}
); }; export default MessageList;