import React, { useState } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; import { Copy, Database, RefreshCw, Rows3, Sigma, ThumbsDown, ThumbsUp, Check, AlertTriangle } from 'lucide-react'; import PipelineTrace from '../pipeline/PipelineTrace'; import SQLBlock from '../artifacts/SQLBlock'; import ResultTable from '../artifacts/ResultTable'; import ChartView from '../artifacts/ChartView'; import MetaBadges from '../artifacts/MetaBadges'; import InsightBlock from '../artifacts/InsightBlock'; import useChatStore from '../../store/useChatStore'; const MarkdownText = React.memo(function MarkdownText({ text = '' }) { const html = String(text) .replace(/&/g, '&') .replace(//g, '>') .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') .replace(/`([^`]+)`/g, '$1') .replace(/\n/g, '
'); return ; }); function extractRows(data) { const rows = data?.answer || data?.data || []; return Array.isArray(rows) && rows.length && typeof rows[0] === 'object' ? rows : []; } function numericColumns(rows) { if (!rows.length) return []; return Object.keys(rows[0]).filter(col => rows.some(row => Number.isFinite(Number(row[col])))); } function formatNumber(value) { return Number(value || 0).toLocaleString(undefined, { maximumFractionDigits: 2 }); } function ResultSummary({ rows }) { if (!rows.length) return null; const nums = numericColumns(rows); const metric = nums[0]; const total = metric ? rows.reduce((sum, row) => sum + Number(row[metric] || 0), 0) : rows.length; const cards = [ { icon: Rows3, label: 'Rows returned', value: formatNumber(rows.length) }, { icon: Sigma, label: metric ? `Total ${metric.replace(/_/g, ' ')}` : 'Records', value: formatNumber(total) }, { icon: Database, label: 'Columns', value: Object.keys(rows[0]).length }, ]; return (
{cards.map(({ icon: Icon, label, value }, i) => (
{label}

{value}

))}
); } function ThinkingStatus({ stage }) { return (
{[0, 1, 2].map(i => ( ))}
{stage || 'Planning retrieval strategy...'}
); } function UserBubble({ content }) { return (
{content}
); } function AssistantBubble({ message, chatId, onRegenerate }) { const addToast = useChatStore(s => s.addToast); const setFeedback = useChatStore(s => s.setFeedback); const [followUpOpen, setFollowUpOpen] = useState(false); const [copied, setCopied] = useState(false); const data = message.data ?? {}; const rows = extractRows(data); const isChatMode = Boolean(message._chatMode); const hasSQL = Boolean(data.sql || message._streamingSql); const pipelineStep = (message.pending || message.streaming) ? (message._pipelineStep ?? 0) : 5; const handleFeedback = async (rating) => { setFeedback(chatId, message.id, rating); try { const { submitFeedback } = await import('../../api/client'); await submitFeedback({ message_id: message.id, user_query: message._userQuery ?? '', generated_sql: data.sql ?? '', rating }); addToast(rating === 'up' ? 'Feedback recorded' : 'Thanks, feedback saved', 'success'); } catch { addToast('Could not save feedback', 'error'); } }; const handleCopyResponse = async () => { const parts = [message.streamText ?? data.message ?? '', data.sql ? `\n\nSQL:\n${data.sql}` : ''].filter(Boolean).join(''); await navigator.clipboard.writeText(parts).catch(() => {}); setCopied(true); addToast('Response copied', 'success'); setTimeout(() => setCopied(false), 2000); }; return (
{/* Assistant avatar */}
S
{/* Main card */}
{message.error ? ( /* Error state with retry option */

{message.error}

) : ( <> {message.pending && !message.data && !message.streamText && !message._streamingSql && !message._pipelineStep && ( )} {Boolean(data.sql) && ( )} {hasSQL && (
)} {rows.length > 0 && } {(message.streamText || data.message) && (
= 4 ? 'typing-cursor block' : ''}`}>
)} {rows.length > 0 && } {rows.length >= 2 && } {/* Follow-up questions */} {Array.isArray(data.follow_ups) && data.follow_ups.length > 0 && !message.streaming && (
{followUpOpen && ( {data.follow_ups.slice(0, 4).map((q, i) => ( ))} )}
)} )}
{/* Action bar */} {!message.pending && !message.streaming && !message.error && (
{/* Left group: Copy & Retry */}
{/* Right group: Feedback */}
)}
); } export default function MessageBubble({ message, chatId, onRegenerate }) { if (message.role === 'user') return ; return ; }