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