import React, { useState, useRef, useEffect } from 'react'; import { useDropzone } from 'react-dropzone'; import { useNavigate, useLocation } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { Send, FileText, TrendingUp, Network, Mic, Paperclip, X, Plus, Trash2, MessageSquare, Pencil, Check, Copy, CheckCheck, Sparkles, ChevronDown, ArrowLeft, Settings, Menu, Database, Eye, Layers, RefreshCw, Square, Zap, Bot, } from 'lucide-react'; import apiService, { api as axiosApi } from '@/services/api'; import { useUserStore } from '@/store/userStore'; import { useConfirmModal } from '@/components/ui/ConfirmModal'; import { useToast } from '@/contexts/ToastContext'; import { getUserIdSync, getAuthHeadersSync } from '@/utils/userId'; // Lazy load PlotlyChart for performance const PlotlyChart = React.lazy(() => import('@/components/PlotlyChart')); // ============================================================================ // ๐ŸŽฏ DIRECT ANSWER BADGE - ChatGPT/Claude Style Highlighted Answer // Shows the key answer prominently (percentage, value, etc.) before full response // ============================================================================ interface DirectAnswer { value: string; type: 'percentage' | 'number' | 'currency' | 'text'; label?: string; trend?: 'up' | 'down' | 'stable'; } const DirectAnswerBadge: React.FC<{ answer: DirectAnswer }> = ({ answer }) => { const getTrendIcon = () => { if (answer.trend === 'up') return '๐Ÿ“ˆ'; if (answer.trend === 'down') return '๐Ÿ“‰'; return ''; }; const getTrendColor = () => { if (answer.trend === 'up') return 'text-green-400'; if (answer.trend === 'down') return 'text-red-400'; return 'text-blue-400'; }; return (
{answer.label && ( {answer.label} )} {answer.value} {answer.trend && {getTrendIcon()}}
); }; // ============================================================================ // ๐Ÿ” EXTRACT DIRECT ANSWER - Finds key values to highlight // ============================================================================ const extractDirectAnswer = (content: string, query: string): DirectAnswer | null => { const queryLower = query.toLowerCase(); // Check if user is asking for a specific metric const percentageKeywords = ['percent', '%', 'rate', 'growth', 'increase', 'decrease', 'ratio', 'proportion', 'share']; const currencyKeywords = ['revenue', 'sales', 'profit', 'cost', 'price', 'total', 'amount', 'value', '$', 'โ‚น', 'โ‚ฌ']; const countKeywords = ['count', 'number', 'how many', 'total', 'quantity']; const isPercentageQuery = percentageKeywords.some(kw => queryLower.includes(kw)); const isCurrencyQuery = currencyKeywords.some(kw => queryLower.includes(kw)); const isCountQuery = countKeywords.some(kw => queryLower.includes(kw)); // Extract percentage values (e.g., "45.2%", "45%", "45.2 percent") const percentMatch = content.match(/(\d+\.?\d*)\s*%/); if (percentMatch && isPercentageQuery) { const value = parseFloat(percentMatch[1]); return { value: `${value}%`, type: 'percentage', trend: content.toLowerCase().includes('increase') || content.toLowerCase().includes('growth') || content.toLowerCase().includes('up') ? 'up' : content.toLowerCase().includes('decrease') || content.toLowerCase().includes('decline') || content.toLowerCase().includes('down') ? 'down' : undefined }; } // Extract currency values (e.g., "$1,234.56", "โ‚น45,000", "โ‚ฌ1.2M") const currencyMatch = content.match(/[\$โ‚ฌโ‚นยฃยฅ][\d,]+\.?\d*\s*[KMBkmb]?/); if (currencyMatch && isCurrencyQuery) { return { value: currencyMatch[0], type: 'currency', trend: content.toLowerCase().includes('increase') || content.toLowerCase().includes('growth') ? 'up' : content.toLowerCase().includes('decrease') || content.toLowerCase().includes('decline') ? 'down' : undefined }; } // Extract large numbers with formatting (e.g., "1,234", "45K", "2.5M") const numberMatch = content.match(/\b(\d{1,3}(?:,\d{3})*(?:\.\d+)?)\s*([KMBkmb])?\b/); if (numberMatch && isCountQuery) { let value = numberMatch[0]; return { value: value, type: 'number' }; } return null; }; // ============================================================================ // ๐Ÿ›ก๏ธ USER-FRIENDLY ERROR MESSAGE FORMATTER // Shows nice messages like Claude/ChatGPT instead of raw API errors // ============================================================================ const formatUserFriendlyError = (error: string | any): string => { const errorStr = typeof error === 'string' ? error.toLowerCase() : (error?.message || error?.response?.data?.detail || String(error)).toLowerCase(); // Rate limit / Service busy errors if (errorStr.includes('rate') || errorStr.includes('limit') || errorStr.includes('429') || errorStr.includes('quota') || errorStr.includes('too many requests')) { return `โณ **Service Temporarily Busy** Our AI service is experiencing high demand. Please wait a moment and try again. ๐Ÿ’ก **Tips:** โ€ข Wait 30 seconds before retrying โ€ข Try asking a simpler question โ€ข Your data is safe and ready`; } // API Key / Auth errors if (errorStr.includes('api_key') || errorStr.includes('unauthorized') || errorStr.includes('authentication') || errorStr.includes('401') || errorStr.includes('invalid key')) { return `๐Ÿ”‘ **Configuration Required** The AI service needs to be configured. Please contact your administrator. _This is a server-side configuration issue._`; } // Connection / Timeout errors if (errorStr.includes('timeout') || errorStr.includes('connection') || errorStr.includes('network') || errorStr.includes('fetch') || errorStr.includes('econnrefused')) { return `๐ŸŒ **Connection Issue** Unable to reach the AI service. Please check your internet connection and try again. ๐Ÿ’ก **Tips:** โ€ข Check your internet connection โ€ข Refresh the page โ€ข Try again in a few moments`; } // Context too large if (errorStr.includes('context') || errorStr.includes('too large') || errorStr.includes('token') || errorStr.includes('length') || errorStr.includes('maximum')) { return `๐Ÿ“ **Request Too Large** Your question or data exceeds the model's capacity. ๐Ÿ’ก **Tips:** โ€ข Try asking a more specific question โ€ข Break down complex queries into smaller parts โ€ข Focus on specific columns or time periods`; } // Server errors if (errorStr.includes('500') || errorStr.includes('502') || errorStr.includes('503') || errorStr.includes('504') || errorStr.includes('internal server')) { return `๐Ÿ”ง **Service Maintenance** Our AI service is temporarily unavailable. We're working on it! Please try again in a few minutes.`; } // Model not found if (errorStr.includes('model') && (errorStr.includes('not found') || errorStr.includes('unavailable'))) { return `๐Ÿค– **AI Model Unavailable** The requested AI model is temporarily unavailable. Trying alternative models... Please try again in a moment.`; } // Generic fallback - don't show raw technical errors if (errorStr.includes('error') || errorStr.includes('exception') || errorStr.includes('failed')) { return `โš ๏ธ **Something Went Wrong** I encountered an issue processing your request. Please try again. ๐Ÿ’ก **Tips:** โ€ข Rephrase your question โ€ข Try a simpler query โ€ข If the problem persists, refresh the page`; } // If nothing matches, return a clean generic message return `โš ๏ธ **Unable to Process Request** Please try again in a moment. If the issue persists, try refreshing the page.`; }; // Helper function to format inline text (bold, italic, code) const formatInlineText = (text: string): string => { return text .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/\*(.+?)\*/g, '$1') .replace(/`([^`]+)`/g, '$1'); }; // Component to render formatted text with inline styles const InlineFormattedText: React.FC<{ text: string; className?: string }> = ({ text, className = '' }) => { const formatted = formatInlineText(text); return ; }; // Image with download/copy buttons const ChartImage: React.FC<{ src: string; alt: string }> = ({ src, alt }) => { const [copied, setCopied] = React.useState(false); const handleDownload = () => { const link = document.createElement('a'); link.href = src; link.download = `chart_${Date.now()}.png`; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; const handleCopy = async () => { try { if (src.startsWith('data:image')) { const response = await fetch(src); const blob = await response.blob(); await navigator.clipboard.write([ new ClipboardItem({ [blob.type]: blob }) ]); } setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error('Failed to copy image:', err); } }; return (
{alt}
{alt &&

{alt}

}
); }; // ============================================================================= // ๐Ÿ–ฅ๏ธ V5: INTERACTIVE CODE BLOCK โ€” Inline Python Execution (AI Chat v2) // ============================================================================= const InteractiveCodeBlock: React.FC<{ code: string; language: string; isPython: boolean; }> = ({ code, language, isPython }) => { const [isRunning, setIsRunning] = React.useState(false); const [result, setResult] = React.useState<{ success?: boolean; stdout?: string; stderr?: string; figures?: { base64: string; format: string }[]; tables?: { columns: string[]; data: any[][]; shape?: number[] }[]; error?: string; execution_time_ms?: number; } | null>(null); const [copied, setCopied] = React.useState(false); const handleRun = async () => { setIsRunning(true); setResult(null); try { const userId = getUserIdSync(); const headers = getAuthHeadersSync(); const baseUrl = axiosApi.defaults.baseURL || ''; const res = await fetch(`${baseUrl}/api/v1/chat/execute-code`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify({ code, user_id: userId }), }); const data = await res.json(); setResult(data); } catch (err: any) { setResult({ success: false, error: err.message, stdout: '', stderr: err.message, figures: [], tables: [] }); } finally { setIsRunning(false); } }; const handleCopy = () => { navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 2000); }; return (
{/* Header */}
{language || 'code'}
{isPython && ( )}
{/* Code */}
        {code}
      
{/* Execution Result */} {result && (
{result.success ? 'โœ“ Output' : 'โœ— Error'} {result.execution_time_ms != null && ( {result.execution_time_ms}ms )}
{result.stdout && (
              {result.stdout}
            
)} {(result.stderr || result.error) && (
              {result.stderr || result.error}
            
)} {result.figures && result.figures.length > 0 && (
{result.figures.map((fig, i) => ( {`Figure ))}
)} {result.tables && result.tables.length > 0 && (
{result.tables.map((table, i) => (
{table.shape &&

DataFrame: {table.shape[0]} rows ร— {table.shape[1]} cols

} {table.columns.map((col, ci) => ( ))}{table.data.slice(0,20).map((row, ri) => ( {row.map((cell, ci) => ( ))} ))}
{col}
{cell != null ? String(cell) : 'โ€”'}
))}
)}
)}
); }; // Lazy-loaded ForecastChartBlock for rendering prediction charts const ForecastChartBlock: React.FC<{ payload: Record }> = ({ payload }) => { const PredictionChart = React.lazy(() => import('@/components/PredictionChartRenderer').then(m => ({ default: m.PredictionChartRenderer })) ); return ( Loading chart... }> ); }; // PlotlyChartBlock for rendering interactive Plotly charts - MEMOIZED for performance const PlotlyChartBlock: React.FC<{ data: any[]; layout: any }> = React.memo(({ data, layout }) => { // PERFORMANCE FIX: Memoize chart data to prevent unnecessary re-renders const chartData = React.useMemo(() => data, [JSON.stringify(data)]); const chartLayout = React.useMemo(() => ({ ...layout, autosize: true, // Disable animations for better performance transition: { duration: 0 }, }), [JSON.stringify(layout)]); return ( Loading interactive chart... }>
); }, (prevProps, nextProps) => { // Custom comparison - only re-render if data/layout actually changed return JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data) && JSON.stringify(prevProps.layout) === JSON.stringify(nextProps.layout); }); // MLChartBlock - Renders matplotlib/seaborn charts from Predict mode (base64 PNG) const MLChartBlock: React.FC<{ chart: { type: string; image: string; title?: string } }> = ({ chart }) => { const typeIcons: Record = { 'ml_forecast': '๐Ÿ“ˆ', 'ml_importance': '๐Ÿ”‘', 'ml_correlation': '๐Ÿ”—', 'ml_distribution': '๐Ÿ“Š', 'ml_residual': '๐Ÿ“‰', 'ml_summary': '๐Ÿค–' }; const icon = typeIcons[chart.type] || '๐Ÿ“Š'; if (!chart.image) return null; return (
{icon} {chart.title || 'ML Visualization'} Powered by scikit-learn + matplotlib
); }; // MLChartsContainer - Renders multiple ML charts from response const MLChartsContainer: React.FC<{ charts: Array<{ type: string; image: string; title?: string }> }> = ({ charts }) => { if (!charts || charts.length === 0) return null; return (
ML Visualizations โ€ข Real scikit-learn analysis
{charts.map((chart, index) => ( ))}
); }; // Typewriter Animation Component - ChatGPT-like word-by-word reveal - Memoized for Performance const TypewriterText: React.FC<{ content: string; isNew?: boolean; onComplete?: () => void; }> = React.memo(({ content, isNew = false, onComplete }) => { const [displayedContent, setDisplayedContent] = useState(isNew ? '' : content); const [isComplete, setIsComplete] = useState(!isNew); const hasAnimatedRef = useRef(false); const intervalRef = useRef(null); const onCompleteRef = useRef(onComplete); // Keep onComplete ref updated without causing re-renders useEffect(() => { onCompleteRef.current = onComplete; }, [onComplete]); useEffect(() => { // Clear any existing interval if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } // If not a new message OR already animated, show full content immediately if (!isNew || hasAnimatedRef.current) { setDisplayedContent(content); setIsComplete(true); return; } // PERFORMANCE FIX: Skip animation for content with charts/visualizations // Charts cause heavy re-renders during typewriter animation const hasCharts = content.includes('plotly_chart') || content.includes('forecast_chart') || content.includes('"data"') && content.includes('"layout"') || content.includes('```') && content.length > 2000; if (hasCharts) { // Show immediately without animation for chart content setDisplayedContent(content); setIsComplete(true); hasAnimatedRef.current = true; onCompleteRef.current?.(); return; } // Mark as animating hasAnimatedRef.current = true; // Reset for new content setDisplayedContent(''); setIsComplete(false); // Split content into words for word-by-word animation const words = content.split(/(\s+)/); let currentIndex = 0; // PERFORMANCE FIX: Use larger chunks (8 words) and longer interval (35ms) intervalRef.current = setInterval(() => { if (currentIndex < words.length) { // Add 8 words at a time for faster, less CPU-intensive animation const nextIndex = Math.min(currentIndex + 8, words.length); const wordsToAdd = words.slice(currentIndex, nextIndex).join(''); setDisplayedContent(prev => prev + wordsToAdd); currentIndex = nextIndex; } else { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } setIsComplete(true); onCompleteRef.current?.(); } }, 35); // 35ms per chunk - less CPU intensive return () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; }, [content, isNew]); // Removed onComplete from deps - using ref instead return ( <> {!isComplete && } ); }); // Component to format markdown-like responses - Memoized for Performance const FormattedMessage: React.FC<{ content: string }> = React.memo(({ content }) => { const formatContent = (text: string) => { let normalizedText = text.replace(/\r\n/g, '\n'); // PRE-PROCESS: Compact any pretty-printed JSON in chart blocks // This fixes the issue where JSON gets split across paragraphs normalizedText = normalizedText.replace( /```plotly_chart\s*([\s\S]*?)```/gi, (match, jsonContent) => { try { // Try to parse and re-stringify as compact JSON const parsed = JSON.parse(jsonContent.trim()); return '```plotly_chart\n' + JSON.stringify(parsed) + '\n```'; } catch (e) { // If parsing fails, try to compact manually (remove newlines/spaces in JSON) const compacted = jsonContent.replace(/\n\s*/g, '').replace(/\s{2,}/g, ' ').trim(); return '```plotly_chart\n' + compacted + '\n```'; } } ); // Also handle JSON blocks that might have "type":"plotly" normalizedText = normalizedText.replace( /(\{[^{}]*"type"\s*:\s*"plotly"[^{}]*"data"[\s\S]*?"layout"[\s\S]*?\}(?:\s*\})*)/gi, (match) => { try { // Find complete JSON by brace matching let braceCount = 0; let startIdx = match.indexOf('{'); let endIdx = startIdx; for (let i = startIdx; i < match.length; i++) { if (match[i] === '{') braceCount++; if (match[i] === '}') braceCount--; if (braceCount === 0) { endIdx = i + 1; break; } } const jsonStr = match.substring(startIdx, endIdx); const parsed = JSON.parse(jsonStr); return '```plotly_chart\n' + JSON.stringify(parsed) + '\n```'; } catch (e) { return match; } } ); const blocks: string[] = []; let currentBlock = ''; let insideCodeBlock = false; const lines = normalizedText.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const isEmptyLine = line.trim() === ''; // Track if we're inside a code block (``` markers) if (line.trim().startsWith('```')) { insideCodeBlock = !insideCodeBlock; } // Don't split inside code blocks - keep them together if (insideCodeBlock) { currentBlock += (currentBlock ? '\n' : '') + line; } else if (isEmptyLine && currentBlock.trim()) { blocks.push(currentBlock.trim()); currentBlock = ''; } else { currentBlock += (currentBlock ? '\n' : '') + line; } } if (currentBlock.trim()) { blocks.push(currentBlock.trim()); } return blocks.map((para, idx) => { if (!para.trim()) return null; // Headers if (para.startsWith('###')) { const headerText = para.replace(/^###\s*/, ''); return

; } if (para.startsWith('##')) { const headerText = para.replace(/^##\s*/, ''); return

; } if (para.startsWith('#')) { const headerText = para.replace(/^#\s*/, ''); return

; } // Code blocks - special handling for forecast_chart // Handle BOTH proper (```) and malformed (``) backticks const isForecastChartBlock = /^`{2,3}\s*forecast_chart/i.test(para); const hasChartTypeJson = para.includes('"chart_type"') && para.includes('"forecast'); if (para.startsWith('```') || para.startsWith('``') || isForecastChartBlock || hasChartTypeJson) { // Check if it's a forecast chart - handle variations with whitespace and malformed backticks const isForecastChart = /^`{2,3}\s*forecast_chart/i.test(para) || hasChartTypeJson; if (isForecastChart) { // Extract JSON with improved regex that handles both ``` and `` backticks let chartJson = ''; // Try triple backticks first let match = para.match(/^`{2,3}\s*forecast_chart\s*([\s\S]*?)`{2,3}\s*$/); if (match) { chartJson = match[1].trim(); } else { // Fallback: try to find JSON object directly const jsonMatch = para.match(/\{[\s\S]*"chart_type"[\s\S]*\}/); if (jsonMatch) { chartJson = jsonMatch[0].trim(); } } // Skip if empty if (!chartJson) { // Check if the paragraph IS the JSON (no backticks at all) if (para.trim().startsWith('{') && para.includes('"chart_type"')) { chartJson = para.trim(); } } if (chartJson) { try { const chartPayload = JSON.parse(chartJson); // Validate it has required chart properties if (chartPayload && chartPayload.chart_type) { return (
); } } catch (e) { console.error('Chart JSON parse error:', e, 'JSON:', chartJson.substring(0, 200)); // Show user-friendly error instead of raw JSON return (

โš ๏ธ Chart rendering failed. The data is available but couldn't be visualized.

); } } } // Check if it's a Plotly chart - improved detection const isPlotlyChart = /^`{2,3}\s*plotly_chart/i.test(para) || para.includes('"type":"plotly"') || (para.includes('"data"') && para.includes('"layout"')); if (isPlotlyChart) { console.log('[CHART DEBUG] Detected plotly_chart block:', para.substring(0, 100)); // Try multiple patterns to extract JSON - BRACE COUNTING IS MOST RELIABLE let chartJson = ''; // PATTERN 1 (PRIMARY): Use brace-counting for complete JSON extraction // This is the most reliable method for nested JSON if (para.includes('{') && (para.includes('"data"') || para.includes('"type"'))) { const startIdx = para.indexOf('{'); if (startIdx !== -1) { let braceCount = 0; let endIdx = para.length; for (let i = startIdx; i < para.length; i++) { if (para[i] === '{') braceCount++; if (para[i] === '}') braceCount--; if (braceCount === 0) { endIdx = i + 1; break; } } chartJson = para.substring(startIdx, endIdx); console.log('[CHART] Brace-counting extracted', chartJson.length, 'chars'); } } // PATTERN 2 (FALLBACK): Regex for plotly_chart code block if (!chartJson) { const match = para.match(/`{2,3}\s*plotly_chart\s*\n?([\s\S]+?)\n?`{2,3}/); if (match) { chartJson = match[1].trim(); console.log('[CHART] Regex extracted', chartJson.length, 'chars'); } } console.log('[CHART DEBUG] Extracted JSON length:', chartJson.length, 'First 100 chars:', chartJson.substring(0, 100)); if (chartJson) { try { const plotlyData = JSON.parse(chartJson); console.log('[CHART DEBUG] Parsed plotly data:', { hasData: !!plotlyData.data, hasLayout: !!plotlyData.layout, chartType: plotlyData.chart_type, dataLength: Array.isArray(plotlyData.data) ? plotlyData.data.length : 0 }); if (plotlyData && plotlyData.data && plotlyData.layout) { return (
); } else { console.error('[CHART DEBUG] Missing data or layout:', { hasData: !!plotlyData.data, hasLayout: !!plotlyData.layout, keys: Object.keys(plotlyData) }); } } catch (e) { console.error('Plotly chart parse error:', e, 'JSON sample:', chartJson.substring(0, 200)); return (

โš ๏ธ Interactive chart rendering failed. Check console for details.

); } } } // Regular code block โ€” with V5 inline execution for Python const langMatch = para.match(/^```(\w+)/); const lang = langMatch ? langMatch[1].toLowerCase() : ''; const codeContent = para.replace(/^```\w*\n?/, '').replace(/```$/, ''); const isPython = lang === 'python' || lang === 'py' || (!lang && (codeContent.includes('import pandas') || codeContent.includes('import numpy') || codeContent.includes('print(') || codeContent.includes('df.'))); return ( ); } // Images (base64 charts) const imageMatch = para.match(/!\[([^\]]*)\]\(([^)]+)\)/); if (imageMatch) { return ; } // Tables const tableLines = para.split('\n').filter(l => l.trim().startsWith('|') || l.includes('|')); if (tableLines.length >= 2) { const isSeparatorLine = (line: string) => /^[\s|:\-]+$/.test(line.replace(/\|/g, '')); const hasSeparator = tableLines.length > 1 && isSeparatorLine(tableLines[1]); const headers = tableLines[0].split('|').map(h => h.trim()).filter(h => h); const dataStartIndex = hasSeparator ? 2 : 1; const rows = tableLines.slice(dataStartIndex) .filter(row => !isSeparatorLine(row)) .map(row => row.split('|').map(cell => cell.trim()).filter(cell => cell)) .filter(row => row.length > 0); if (headers.length > 0 && rows.length > 0) { return (
{headers.map((header, i) => ( ))} {rows.map((row, i) => ( {row.map((cell, j) => ( ))} ))}
); } } // Lists const paraLines = para.split('\n'); const bulletLines = paraLines.filter(line => line.trim().match(/^[-โ€ข*]\s/) || line.trim().match(/^[โ€ขโ—]\s?/)); const numberedLines = paraLines.filter(line => line.trim().match(/^\d+[\.\\)]\s/)); if (bulletLines.length > 0 && bulletLines.length >= paraLines.length * 0.4) { return (
    {paraLines.map((line, i) => { const cleanLine = line.trim(); if (cleanLine.match(/^[-โ€ข*โ—]\s?/) || cleanLine.match(/^[โ€ขโ—]/)) { const text = cleanLine.replace(/^[-โ€ข*โ—]\s*/, ''); return (
  • โ€ข
  • ); } return cleanLine ?

    : null; })}
); } if (numberedLines.length > 0 && numberedLines.length >= paraLines.length * 0.4) { return (
    {paraLines.map((line, i) => { const cleanLine = line.trim(); const match = cleanLine.match(/^(\d+)[\.\\)]\s*(.*)$/); if (match) { return (
  1. {match[1]}.
  2. ); } return cleanLine ?

    : null; })}
); } // Regular paragraph const formattedHtml = formatInlineText(para).replace(/\n/g, '
'); return (

); }); }; return

{formatContent(content)}
; }); // Animated Logo for AI Assistant - Uses actual DataVision logo with subtle animation // Adapts to light/dark mode automatically const AnimatedBotIcon: React.FC<{ size?: number; isDark?: boolean }> = ({ size = 28, isDark = true }) => { // Determine the logo to use based on theme const logoSrc = isDark ? '/datavision-logo-dark.jpg' : '/datavision-logo-light.jpg'; return (
DataVision { (e.target as HTMLImageElement).src = '/logo.svg'; }} />
); }; // Mode-specific thinking messages (Clean & Simple) const getModeThinkingText = (modeId: string): string => { const thinkingTexts: Record = { 'analyst': 'Analyzing data...', 'deep': 'Thinking deeply...', 'vision': 'Analyzing image...', 'predict': 'Running prediction...', 'agent': 'Taking action...', }; return thinkingTexts[modeId] || 'Processing...'; }; // Sleek, minimal Thinking Animation Indicator const TypingIndicator: React.FC<{ mode?: string; isDark?: boolean }> = ({ mode = 'rag', isDark = true }) => { const thinkingText = getModeThinkingText(mode); return (
{thinkingText}
); }; // ChatGPT-Style Welcome Screen with Centered Input (includes Mode & MCP controls) interface WelcomeScreenProps { onSuggestionClick: (text: string) => void; input: string; setInput: (val: string) => void; onSend: () => void; isLoading: boolean; onFileClick: () => void; onPaste: (event: React.ClipboardEvent) => void; // ๐Ÿ“‹ Added paste handler for new chat hasAttachment: boolean; // ๐Ÿ“Ž Enable send button when file attached imagePreview: string | null; // ๐Ÿ–ผ๏ธ Show image preview onRemoveImage: () => void; // โŒ Remove image currentMode: { id: string; label: string; icon: any }; onModeClick: () => void; onMcpClick: () => void; mcpCount: { active: number; total: number }; llm: string; setLlm: (val: string) => void; } const WelcomeScreen: React.FC = ({ onSuggestionClick, input, setInput, onSend, isLoading, onFileClick, onPaste, // ๐Ÿ“‹ Added paste handler hasAttachment, // ๐Ÿ“Ž Track if file attached imagePreview, // ๐Ÿ–ผ๏ธ Image preview URL onRemoveImage, // โŒ Remove image handler currentMode, onModeClick, onMcpClick, mcpCount, llm, setLlm }) => { const suggestions = [ { icon: TrendingUp, text: "Analyze trends in my data" }, { icon: FileText, text: "Summarize my uploaded data" }, { icon: Sparkles, text: "What insights can you find?" }, { icon: Network, text: "Find patterns and correlations" }, ]; // Use state for logo to ensure reactivity const [logoSrc, setLogoSrc] = React.useState('/datavision-logo-dark.jpg'); React.useEffect(() => { const updateLogo = () => { const isDarkTheme = !document.documentElement.classList.contains('light-theme'); setLogoSrc(isDarkTheme ? '/datavision-logo-dark.jpg' : '/datavision-logo-light.jpg'); }; updateLogo(); // Listen for theme changes const observer = new MutationObserver(updateLogo); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); return () => observer.disconnect(); }, []); return (
{/* Logo - Theme Aware */}
DataVision { (e.target as HTMLImageElement).src = '/logo.svg'; }} />
{/* Title - ChatGPT style */}

How can I help you today?

{/* ๐Ÿ–ผ๏ธ Image Preview - Show attached image above input */} {imagePreview && (
Attached

๐Ÿ“ท Image ready - ask a question about it!

)} {/* Centered Search Box - ChatGPT Style with Mode & MCP */}
{/* MCP Button */} {/* File Attach Button */} {/* Input Field */} setInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); onSend(); } }} onPaste={onPaste} placeholder="Ask anything about your data..." className="flex-1 bg-transparent text-white placeholder-gray-500 py-4 px-2 outline-none text-base" /> {/* Mode Selector Button */} {/* Send Button */}
{/* Suggestion Cards - 2x2 Grid */}
{suggestions.map((suggestion, i) => ( ))}
); }; const AnalystChat: React.FC = () => { const navigate = useNavigate(); const location = useLocation(); const { conversations, currentConversationId, createConversation, deleteConversation, selectConversation, addMessageToConversation, updateConversationMessages, getCurrentConversation, clearAllChats, isDark, } = useUserStore(); const { confirm, ConfirmModal } = useConfirmModal(); const toast = useToast(); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const [mode, setMode] = useState('analyst'); const [llm, setLlm] = useState('llama'); const [isFullscreen, setIsFullscreen] = useState(false); // Advanced Progressive Loading States const [loadingMessage, setLoadingMessage] = useState("Initializing DataVision intelligence..."); const loadingMessages = [ "Initializing DataVision intelligence...", "Scanning dataset structure...", "Identifying patterns and correlations...", "Applying predictive models...", "Synthesizing insights...", "Finalizing response...", "Almost there..." ]; useEffect(() => { let interval: NodeJS.Timeout; if (isLoading) { let currentIndex = 0; setLoadingMessage(loadingMessages[0]); interval = setInterval(() => { currentIndex = (currentIndex + 1) % loadingMessages.length; // Stop at the last message to avoid looping back to "Initializing..." after a long time if (currentIndex === loadingMessages.length - 1) { clearInterval(interval); } setLoadingMessage(loadingMessages[currentIndex]); }, 2500); } return () => clearInterval(interval); }, [isLoading]); const [isRecording, setIsRecording] = useState(false); const [selectedImage, setSelectedImage] = useState(null); const [imagePreviewUrl, setImagePreviewUrl] = useState(null); const [attachedFiles, setAttachedFiles] = useState([]); const [editingMessageId, setEditingMessageId] = useState(null); const [editingContent, setEditingContent] = useState(''); const [copiedMessageId, setCopiedMessageId] = useState(null); const [messageImages, setMessageImages] = useState>({}); const [modeDropdownOpen, setModeDropdownOpen] = useState(false); const [animatingMessageId, setAnimatingMessageId] = useState(null); // Track which message is animating const [sidebarOpen, setSidebarOpen] = useState(true); const [mcpDropdownOpen, setMcpDropdownOpen] = useState(false); const [plusMenuOpen, setPlusMenuOpen] = useState(false); // Plus menu state // ๐Ÿ† 8 Powerful MCPs - all enabled by default const [enabledMcps, setEnabledMcps] = useState>({ 'data_analyzer': true, 'pattern_finder': true, 'forecaster': true, // NEW: ML predictions 'anomaly_detector': true, // NEW: Outlier detection 'chart_executor': true, // NEW: Auto-visualization 'report_generator': true, 'insight_extractor': true, 'comparison_engine': true, }); // MCP execution state for Claude-style animations const [mcpExecutionStatus, setMcpExecutionStatus] = useState<{ isRunning: boolean; currentTools: Array<{ name: string; icon: string; status: 'pending' | 'running' | 'success' }>; startTime: number | null; }>({ isRunning: false, currentTools: [], startTime: null, }); // MCP Permission Dialog state (Claude-style Allow/Deny) const [mcpPermissionDialog, setMcpPermissionDialog] = useState<{ isOpen: boolean; pendingTools: Array<{ name: string; icon: string; description: string }>; pendingMessage: string; onAllow: (() => void) | null; onDeny: (() => void) | null; }>({ isOpen: false, pendingTools: [], pendingMessage: '', onAllow: null, onDeny: null, }); // Streaming state for ChatGPT-like word-by-word responses const [useStreaming, setUseStreaming] = useState(false); // Disabled - use reliable non-streaming mode const [streamingContent, setStreamingContent] = useState(''); const [streamingMessageId, setStreamingMessageId] = useState(null); const messagesEndRef = useRef(null); const chatContainerRef = useRef(null); const fileInputRef = useRef(null); const textareaRef = useRef(null); const abortControllerRef = useRef(null); // Feedback state for thumbs up/down const [messageFeedback, setMessageFeedback] = useState>({}); // Handle stop generation (ChatGPT-style stop button in input area) const handleStopGeneration = () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); abortControllerRef.current = null; } setIsLoading(false); }; // Handle thumbs up/down feedback const handleFeedback = (messageId: string, type: 'up' | 'down') => { setMessageFeedback(prev => ({ ...prev, [messageId]: prev[messageId] === type ? null : type })); // You could also send this to the backend for analytics console.log(`Feedback ${type} for message ${messageId}`); }; // ๐Ÿ† 5 POWERFUL MCPs - Competition-Winning Tools // Each MCP is a unique powerhouse that works autonomously const mcpServers = [ { id: 'data_analyzer', name: '๐Ÿ“Š Data Analyzer', icon: '๐Ÿ“Š', description: 'Deep data exploration โ€ข Schema analysis โ€ข Quality checks', power: 'Explores any dataset structure autonomously' }, { id: 'pattern_finder', name: '๐Ÿ”Ž Pattern Finder', icon: '๐Ÿ”Ž', description: 'Anomaly detection โ€ข Trends โ€ข Correlations', power: 'Discovers hidden patterns you never noticed' }, { id: 'forecaster', name: '๐Ÿ”ฎ Forecaster', icon: '๐Ÿ”ฎ', description: 'ML predictions โ€ข Confidence intervals โ€ข Trend projection', power: 'Predicts future with scikit-learn algorithms' }, { id: 'anomaly_detector', name: 'โš ๏ธ Anomaly Detector', icon: 'โš ๏ธ', description: 'Outlier detection โ€ข Z-score analysis โ€ข Risk alerts', power: 'Finds statistical anomalies automatically' }, { id: 'chart_executor', name: '๐Ÿ“ˆ Chart Generator', icon: '๐Ÿ“ˆ', description: 'Auto-visualization โ€ข Intent detection โ€ข Dynamic colors', power: 'Creates perfect charts from natural language' }, { id: 'report_generator', name: '๐Ÿ“„ Report Generator', icon: '๐Ÿ“„', description: 'Executive summaries โ€ข Key metrics โ€ข Recommendations', power: 'Creates professional reports in seconds' }, { id: 'insight_extractor', name: '๐Ÿ’ก Insight Extractor', icon: '๐Ÿ’ก', description: 'Business opportunities โ€ข Pareto analysis โ€ข Growth potential', power: 'Mines the most valuable insights from your data' }, { id: 'comparison_engine', name: 'โš–๏ธ Comparison Engine', icon: 'โš–๏ธ', description: 'Category comparison โ€ข Segment analysis โ€ข Benchmarks', power: 'Compares data across any dimension' }, ]; // Mode definitions - 5 CORE MODES (Clean & Professional) // Each mode has a specific purpose const modes = [ // ๐Ÿ“Š ANALYST - Default mode, smart data analysis { id: 'analyst', label: '๐Ÿ“Š Analyst', icon: FileText, description: 'Smart data analysis with charts & insights', fullDescription: 'Upload data and ask questions - get instant analysis with visualizations', features: ['Data Analysis', 'Auto Charts', 'Insights', 'Summaries'], badge: null, isAI: false, category: 'core', supportsText: true, supportsCharts: true, color: 'from-blue-500 to-cyan-500' }, // ๐Ÿง  DEEP THINK - Complex reasoning { id: 'deep', label: '๐Ÿง  Deep Think', icon: Sparkles, description: 'Multi-step reasoning for complex questions', fullDescription: 'Think deeper - breaks down complex problems step by step', features: ['Deep Research', 'Multi-Step', 'Reasoning', 'Analysis'], badge: 'Pro', isAI: true, category: 'core', supportsText: true, supportsCharts: true, color: 'from-purple-500 to-pink-500' }, // ๐Ÿ‘๏ธ VISION - Image analysis { id: 'vision', label: '๐Ÿ‘๏ธ Vision', icon: Eye, description: 'Analyze images, charts, documents & screenshots', fullDescription: 'Upload any image and ask questions about it', features: ['Image Analysis', 'Chart Reading', 'OCR', 'Document Scan'], badge: null, isAI: false, category: 'core', supportsText: true, supportsCharts: true, supportsImages: true, color: 'from-green-500 to-emerald-500' }, // ๐Ÿ“ˆ PREDICT - ML forecasting { id: 'predict', label: '๐Ÿ“ˆ Predict', icon: TrendingUp, description: 'ML predictions, forecasts & trend analysis', fullDescription: 'Predict future trends using machine learning', features: ['ML Forecasts', 'Trends', 'Predictions', 'Confidence'], badge: 'ML', isAI: true, category: 'core', supportsText: true, supportsCharts: true, color: 'from-amber-500 to-orange-500' }, // ๐Ÿค– AGENT - Autonomous AI { id: 'agent', label: '๐Ÿค– Agent', icon: Bot, description: 'Autonomous AI that takes actions on your data', fullDescription: 'Let AI plan and execute multi-step data tasks', features: ['Auto Actions', 'Multi-Step', 'Tools', 'Planning'], badge: 'Pro', isAI: true, category: 'core', supportsText: true, supportsCharts: true, color: 'from-red-500 to-rose-500' }, ]; const currentConversation = getCurrentConversation(); const messages = currentConversation?.messages.map(m => ({ ...m, timestamp: new Date(m.timestamp) })) || []; // Auto-send prefilled message from other pages (like Anomaly Monitor) useEffect(() => { if (location.state?.prefillMessage) { const msg = location.state.prefillMessage; // Clear the state so it doesn't fire again if the user refreshes the page navigate(location.pathname, { replace: true, state: {} }); // Small delay to ensure state and component are fully mounted setTimeout(() => { handleSend(msg); }, 500); } }, [location.state, navigate]); useEffect(() => { if (!currentConversationId && conversations.length > 0) { selectConversation(conversations[0].id); } }, [currentConversationId, conversations]); useEffect(() => { if (currentConversation) { setMode(currentConversation.mode); // Restore images from stored imageData in messages const restoredImages: Record = {}; currentConversation.messages.forEach((msg: any) => { if (msg.imageData) { restoredImages[msg.id] = msg.imageData; } }); if (Object.keys(restoredImages).length > 0) { setMessageImages(prev => ({ ...prev, ...restoredImages })); } } }, [currentConversation?.id]); // Listen for file updates from DataHub - notify user when new files are available useEffect(() => { const handleFilesUpdated = () => { console.log('๐Ÿ“ Files updated in chat context'); // Add a system notification message to let user know data has been refreshed if (currentConversationId) { const systemMessage = { id: `system_${Date.now()}`, role: 'assistant' as const, content: '๐Ÿ“ **Data Updated!** New files have been uploaded. Your data context has been refreshed. Feel free to ask questions about your updated dataset.', timestamp: new Date().toISOString(), }; addMessageToConversation(currentConversationId, systemMessage); } }; window.addEventListener('filesUpdated', handleFilesUpdated); return () => window.removeEventListener('filesUpdated', handleFilesUpdated); }, [currentConversationId, addMessageToConversation]); // ========================================================================= // CHATGPT-STYLE SCROLL LOGIC - OPTIMIZED TO PREVENT JITTER // ========================================================================= // Track previous message count to detect actual new messages const prevMessageCountRef = useRef(messages.length); const prevLoadingRef = useRef(isLoading); const userScrolledUpRef = useRef(false); // Check if user is near the bottom of the chat (within 200px) const isNearBottom = (): boolean => { const container = chatContainerRef.current; if (!container) return true; return container.scrollHeight - container.scrollTop - container.clientHeight < 200; }; // Scroll to bottom - instant for new messages, smooth for loading const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => { if (messagesEndRef.current) { messagesEndRef.current.scrollIntoView({ behavior, block: 'end' }); } }; // Track user scroll position to avoid interrupting reading useEffect(() => { const container = chatContainerRef.current; if (!container) return; const handleScroll = () => { // Mark that user has scrolled up if not near bottom userScrolledUpRef.current = !isNearBottom(); }; container.addEventListener('scroll', handleScroll, { passive: true }); return () => container.removeEventListener('scroll', handleScroll); }, []); // Auto-scroll ONLY when: // 1. A NEW message is added (not re-renders) // 2. Loading state changes to true (user sent a message) // 3. User hasn't scrolled up to read useEffect(() => { const messageCountChanged = messages.length !== prevMessageCountRef.current; const loadingStarted = isLoading && !prevLoadingRef.current; // Update refs prevMessageCountRef.current = messages.length; prevLoadingRef.current = isLoading; // Only scroll if there's an actual new message or user started loading if (messageCountChanged || loadingStarted) { // Reset user scrolled flag when they send a new message if (loadingStarted) { userScrolledUpRef.current = false; } // Don't scroll if user has scrolled up to read earlier messages if (!userScrolledUpRef.current) { // Use requestAnimationFrame for smooth scroll without jitter requestAnimationFrame(() => { scrollToBottom(loadingStarted ? 'smooth' : 'instant'); }); } } }, [messages.length, isLoading]); useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = 'auto'; textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px`; } }, [input]); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop: (acceptedFiles: File[]) => { // Separate images from other files const images = acceptedFiles.filter(f => f.type.startsWith('image/')); const otherFiles = acceptedFiles.filter(f => !f.type.startsWith('image/')); // If there's an image, set it as selected (for vision) if (images.length > 0) { const imageFile = images[0]; setSelectedImage(imageFile); const reader = new FileReader(); reader.onload = (e) => { setImagePreviewUrl(e.target?.result as string); }; reader.readAsDataURL(imageFile); toast.success(`๐Ÿ–ผ๏ธ Image "${imageFile.name}" ready for analysis!`); } // Add other files as attachments if (otherFiles.length > 0) { setAttachedFiles(prev => [...prev, ...otherFiles]); toast.success(`๐Ÿ“Ž ${otherFiles.length} file(s) attached!`); } }, noClick: true, accept: { 'application/pdf': ['.pdf'], 'application/vnd.ms-excel': ['.xls'], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], 'text/csv': ['.csv'], 'application/json': ['.json'], 'text/plain': ['.txt'], 'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'], 'application/vnd.ms-powerpoint': ['.ppt'], 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'], 'application/msword': ['.doc'], 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], }, }); const removeAttachedFile = (index: number) => { setAttachedFiles(prev => prev.filter((_, i) => i !== index)); }; const handleVoiceInput = async () => { if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) { toast.warning('Speech recognition is not supported in your browser.'); return; } const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition; const recognition = new SpeechRecognition(); recognition.lang = 'en-US'; recognition.continuous = false; recognition.onstart = () => setIsRecording(true); recognition.onend = () => setIsRecording(false); recognition.onresult = (event: any) => { const transcript = event.results[0][0].transcript; setInput(prev => prev + ' ' + transcript); }; recognition.onerror = (event: any) => { console.error('Speech recognition error:', event.error); setIsRecording(false); }; recognition.start(); }; const handleImageSelect = (event: React.ChangeEvent) => { const files = event.target.files; if (!files || files.length === 0) return; const fileArray = Array.from(files); let hasImage = false; const otherFiles: File[] = []; for (const file of fileArray) { if (file.type.startsWith('image/')) { // Set first image as the main image for vision if (!hasImage) { setSelectedImage(file); const reader = new FileReader(); reader.onload = (e) => { setImagePreviewUrl(e.target?.result as string); }; reader.readAsDataURL(file); hasImage = true; } } else { // Non-image files go to attachedFiles otherFiles.push(file); } } // Add non-image files as attachments if (otherFiles.length > 0) { setAttachedFiles(prev => [...prev, ...otherFiles]); toast.success(`๐Ÿ“Ž ${otherFiles.length} file(s) attached!`); } // Set default prompt based on what was uploaded if (hasImage && !input.trim()) { setInput('What can you see in this image?'); } else if (otherFiles.length > 0 && !input.trim()) { setInput('Analyze this data'); } // Reset file input event.target.value = ''; }; const handlePaste = async (event: React.ClipboardEvent) => { const items = event.clipboardData?.items; const files = event.clipboardData?.files; if (!items && !files) return; // Check for pasted files if (files && files.length > 0) { event.preventDefault(); for (let i = 0; i < files.length; i++) { const file = files[i]; if (file.type.startsWith('image/')) { // Handle image paste setSelectedImage(file); const reader = new FileReader(); reader.onload = (e) => { setImagePreviewUrl(e.target?.result as string); }; reader.readAsDataURL(file); setInput(prev => prev || 'What can you see in this image?'); } else { // Handle other file types (PDF, Excel, CSV, etc.) const supportedTypes = [ 'application/pdf', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/csv', 'application/json', 'text/plain' ]; if (supportedTypes.includes(file.type) || file.name.match(/\.(pdf|xlsx|xls|csv|json|txt)$/i)) { setAttachedFiles(prev => [...prev, file]); toast.success(`๐Ÿ“Ž File "${file.name}" attached!`); } } } return; } // Check clipboard items for images for (let i = 0; i < (items?.length || 0); i++) { const item = items![i]; if (item.type.startsWith('image/')) { event.preventDefault(); const blob = item.getAsFile(); if (blob) { const file = new File([blob], `pasted_image_${Date.now()}.png`, { type: blob.type }); setSelectedImage(file); const reader = new FileReader(); reader.onload = (e) => { setImagePreviewUrl(e.target?.result as string); }; reader.readAsDataURL(file); setInput(prev => prev || 'What can you see in this image?'); } break; } } }; // Reusable function to process user message (MCPs, API, Animation) const processUserMessage = async ( messageText: string, convId: string, visionAttachments: any[] = [], filesToCompare?: string[] ) => { setIsLoading(true); try { // Check if we should use streaming (for RAG modes without images) const canStream = useStreaming && !visionAttachments.length && ['rag', 'hybrid'].includes(mode); if (canStream) { // STREAMING MODE - Word by word like ChatGPT const assistantMsgId = (Date.now() + 1).toString(); // Create placeholder message that will be updated const streamingAssistantMessage = { id: assistantMsgId, role: 'assistant' as const, content: '', timestamp: new Date().toISOString(), sources: [] as string[], }; addMessageToConversation(convId, streamingAssistantMessage); setStreamingMessageId(assistantMsgId); setStreamingContent(''); let fullContent = ''; // Get model based on selected LLM const model = llm; await apiService.streamMessage( messageText, model, 'rag', // onChunk - update message word by word (chunk: string) => { fullContent += chunk; setStreamingContent(fullContent); // Update the message in conversation const currentConv = getCurrentConversation(); if (currentConv) { const updatedMessages = currentConv.messages.map(m => m.id === assistantMsgId ? { ...m, content: fullContent } : m ); updateConversationMessages(convId, updatedMessages); } }, // onDone () => { setStreamingMessageId(null); setStreamingContent(''); setIsLoading(false); }, // onError (error: string) => { console.error('Streaming error:', error); const currentConv = getCurrentConversation(); if (currentConv) { // Format error as user-friendly message const friendlyError = formatUserFriendlyError(error); const updatedMessages = currentConv.messages.map(m => m.id === assistantMsgId ? { ...m, content: fullContent || friendlyError } : m ); updateConversationMessages(convId, updatedMessages); } setStreamingMessageId(null); setIsLoading(false); } ); } else { // NON-STREAMING MODE - Regular API call with MCP animation let mcpsToUse = enabledMcps; // Detect which MCPs might be triggered based on query (EXPANDED keyword matching) const queryLower = messageText.toLowerCase(); const potentialTools: Array<{ name: string; icon: string; status: 'pending' | 'running' | 'success'; description: string }> = []; // Helper function for partial keyword matching const matchesAny = (keywords: string[]) => keywords.some(kw => queryLower.includes(kw)); // ๐Ÿ† DETECT 5 POWERFUL MCPs based on query if (enabledMcps.data_analyzer && matchesAny(['analyz', 'explore', 'schema', 'structure', 'columns', 'data type', 'overview', 'describe', 'what data', 'statistics', 'stats'])) { potentialTools.push({ name: '๐Ÿ” Data Analyzer', icon: '๐Ÿ”', status: 'pending', description: 'Deep data exploration & schema analysis' }); } if (enabledMcps.pattern_finder && matchesAny(['pattern', 'anomal', 'outlier', 'trend', 'unusual', 'spike', 'drop', 'correlation', 'detect', 'discover', 'hidden'])) { potentialTools.push({ name: '๐Ÿ”Ž Pattern Finder', icon: '๐Ÿ”Ž', status: 'pending', description: 'Finds patterns, anomalies & correlations' }); } if (enabledMcps.report_generator && matchesAny(['report', 'summary', 'executive', 'document', 'presentation', 'findings', 'conclusion', 'recommend'])) { potentialTools.push({ name: '๐Ÿ“„ Report Generator', icon: '๐Ÿ“„', status: 'pending', description: 'Creates professional reports' }); } if (enabledMcps.insight_extractor && matchesAny(['insight', 'key', 'important', 'valuable', 'business', 'opportunit', 'risk', 'growth', 'potential', 'learn'])) { potentialTools.push({ name: '๐Ÿ’ก Insight Extractor', icon: '๐Ÿ’ก', status: 'pending', description: 'Mines valuable insights from data' }); } if (enabledMcps.comparison_engine && matchesAny(['compare', 'versus', 'vs', 'difference', 'between', 'segment', 'category', 'breakdown', 'by', 'top', 'bottom', 'best', 'worst'])) { potentialTools.push({ name: 'โš–๏ธ Comparison Engine', icon: 'โš–๏ธ', status: 'pending', description: 'Compares across dimensions' }); } // ๐Ÿš€ SPEED OPTIMIZATION: Skip MCP permission dialog for faster responses // MCPs are auto-allowed silently (like Claude after first authorization) // This removes the 300-400ms delay per tool and permission dialog wait time if (potentialTools.length > 0) { // Show quick MCP indicator (non-blocking) setMcpExecutionStatus({ isRunning: true, currentTools: potentialTools.slice(0, 2).map((t, idx) => ({ name: t.name, icon: t.icon, status: idx === 0 ? 'running' : 'pending' })), startTime: Date.now(), }); } // ๐Ÿง  BUILD CONVERSATION HISTORY FOR PERSISTENT MEMORY // Get last 10 messages for context (like ChatGPT/Claude) const currentConv = getCurrentConversation(); const recentHistory = currentConv?.messages .slice(-10) .map(m => ({ role: m.role, content: m.content.substring(0, 500) // Limit content length })) || []; const response = await apiService.sendMessage( messageText, llm, mode, convId, filesToCompare && filesToCompare.length > 0 ? filesToCompare : undefined, visionAttachments.length > 0 ? visionAttachments : undefined, mcpsToUse, recentHistory // Pass conversation history for memory ); // Clear MCP animation setMcpExecutionStatus({ isRunning: false, currentTools: [], startTime: null }); const responseContent = response.data?.message || response.data?.answer || 'No response received'; const responseSources = response.data?.sources || []; const responseSuggestions = response.data?.suggestions || []; const responseConfidence = response.data?.confidence; const assistantMessage = { id: (Date.now() + 1).toString(), role: 'assistant' as const, content: String(responseContent), timestamp: new Date().toISOString(), sources: Array.isArray(responseSources) ? responseSources.map(s => String(s)) : [], // ๐Ÿ† Competition-winning features suggestions: responseSuggestions, confidence: responseConfidence, mode: mode, }; addMessageToConversation(convId, assistantMessage); setAnimatingMessageId(assistantMessage.id); // Trigger typewriter animation setIsLoading(false); } } catch (error: any) { console.error('Error processing message:', error); // Check for rate limit error first (added by our interceptor) let friendlyError: string; if (error.isRateLimited) { friendlyError = error.rateLimitMessage; } else if (error.serverErrorMessage) { friendlyError = error.serverErrorMessage; } else { // Format error as user-friendly message const rawError = error.response?.data?.detail || error.message || 'Unknown error'; friendlyError = formatUserFriendlyError(rawError); } const errorMessage = { id: (Date.now() + 1).toString(), role: 'assistant' as const, content: friendlyError, timestamp: new Date().toISOString(), }; addMessageToConversation(convId, errorMessage); setIsLoading(false); setMcpExecutionStatus({ isRunning: false, currentTools: [], startTime: null }); } }; const handleSend = async (eOrText?: React.MouseEvent | string) => { let textOverride: string | undefined; if (typeof eOrText === 'string') { textOverride = eOrText; } const textToProcess = typeof textOverride === 'string' ? textOverride : input; const hasContent = textToProcess.trim().length > 0 || selectedImage || attachedFiles.length > 0; if (!hasContent || isLoading) return; let convId = currentConversationId; if (!convId) { convId = createConversation(mode); } const userMessage: any = { id: Date.now().toString(), role: 'user' as const, content: textToProcess.trim(), timestamp: new Date().toISOString(), imageData: null, }; // If image selected, read as base64 BEFORE adding to conversation if (selectedImage) { const base64Content = await new Promise((resolve) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.readAsDataURL(selectedImage); }); // Store base64 in message for persistence userMessage.imageData = base64Content; // Also set in messageImages state for immediate display setMessageImages(prev => ({ ...prev, [userMessage.id]: base64Content })); } addMessageToConversation(convId, userMessage); // Scroll to bottom immediately after sending message (ChatGPT behavior) setTimeout(() => scrollToBottom('smooth'), 100); const messageToSend = textToProcess.trim(); const filesToCompare = attachedFiles.map(f => f.name); // Create vision attachments let visionAttachments: any[] = []; // Use the already-stored imageData from userMessage if (userMessage.imageData) { visionAttachments.push({ name: 'uploaded_image', type: 'image/png', size: 0, content: userMessage.imageData }); } // Upload files if (attachedFiles.length > 0) { await apiService.uploadFiles(attachedFiles); window.dispatchEvent(new CustomEvent('filesUpdated')); } // Reset Input if we were using the text area if (typeof textOverride !== 'string') { setInput(''); } setSelectedImage(null); setImagePreviewUrl(null); setAttachedFiles([]); // Process Message await processUserMessage(messageToSend, convId, visionAttachments, filesToCompare); }; const handleEditMessage = (messageId: string, content: string) => { setEditingMessageId(messageId); setEditingContent(content); }; const handleCancelEdit = () => { setEditingMessageId(null); setEditingContent(''); }; const handleSaveAndResend = async (messageId: string) => { if (!editingContent.trim() || isLoading) return; const messageIndex = messages.findIndex(m => m.id === messageId); if (messageIndex === -1) return; let convId = currentConversationId || createConversation(mode); const messagesToKeep = messages.slice(0, messageIndex).map(m => ({ ...m, timestamp: m.timestamp instanceof Date ? m.timestamp.toISOString() : m.timestamp })); updateConversationMessages(convId, messagesToKeep); const messageToSend = editingContent.trim(); setEditingMessageId(null); setEditingContent(''); const userMessage = { id: Date.now().toString(), role: 'user' as const, content: messageToSend, timestamp: new Date().toISOString(), }; addMessageToConversation(convId, userMessage); await processUserMessage(messageToSend, convId); }; const handleCopyMessage = async (messageId: string, content: string) => { try { await navigator.clipboard.writeText(content); setCopiedMessageId(messageId); setTimeout(() => setCopiedMessageId(null), 2000); } catch (error) { console.error('Failed to copy:', error); } }; // Handle export message to PDF/PPTX/Email const handleExportMessage = async (content: string, format: 'pdf' | 'pptx' | 'email') => { try { const response = await fetch('/api/v1/exports/generate', { method: 'POST', headers: getAuthHeadersSync(), body: JSON.stringify({ title: 'Business Analyst Report', content: content, format: format, workspace_id: getUserIdSync() }) }); if (!response.ok) throw new Error('Export failed'); const data = await response.json(); // Decode base64 and download const binaryString = atob(data.content_base64); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } const blob = new Blob([bytes], { type: data.content_type }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = data.filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (error: any) { console.error('Export failed:', error); toast.error('Export failed: ' + (error.message || 'Unknown error')); } }; // Handle regenerate response - find the previous user message and resend it const handleRegenerate = async (messageId: string) => { const currentConv = getCurrentConversation(); if (!currentConv) return; // Find the assistant message index const msgIndex = currentConv.messages.findIndex(m => m.id === messageId); if (msgIndex <= 0) return; // Find the previous user message let userMsgIndex = msgIndex - 1; while (userMsgIndex >= 0 && currentConv.messages[userMsgIndex].role !== 'user') { userMsgIndex--; } if (userMsgIndex < 0) return; const userMessage = currentConv.messages[userMsgIndex] as any; // Remove the assistant message we're regenerating const updatedMessages = currentConv.messages.filter((_, i) => i !== msgIndex); updateConversationMessages(currentConv.id, updatedMessages); // Prepare vision attachments if image existed let visionAttachments: any[] = []; if (userMessage.imageData) { visionAttachments.push({ name: 'uploaded_image', type: 'image/png', size: 0, content: userMessage.imageData }); } // Call processUserMessage await processUserMessage(userMessage.content, currentConv.id, visionAttachments); }; const handleNewChat = () => { // Reset all file states for new chat to enable immediate upload setSelectedImage(null); setImagePreviewUrl(null); setAttachedFiles([]); setInput(''); setIsLoading(false); setEditingMessageId(null); setEditingContent(''); // Create new conversation createConversation(mode); }; const handleDeleteConversation = async (id: string, e: React.MouseEvent) => { e.stopPropagation(); const conv = conversations.find(c => c.id === id); const confirmed = await confirm({ title: 'Delete Conversation', message: `Delete "${conv?.title || 'this conversation'}"? This cannot be undone.`, confirmText: 'Delete', cancelText: 'Keep', variant: 'danger', }); if (confirmed) { deleteConversation(id); toast.deleted('Conversation Deleted'); } }; const handleSuggestionClick = (text: string) => { setInput(text); textareaRef.current?.focus(); }; const formatDate = (dateStr: string) => { const date = new Date(dateStr); const now = new Date(); const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24)); if (diffDays === 0) return 'Today'; if (diffDays === 1) return 'Yesterday'; if (diffDays < 7) return `${diffDays} days ago`; return date.toLocaleDateString(); }; const currentMode = modes.find(m => m.id === mode) || modes[0]; // Safe fallback to analyst // Refs for click-outside detection const mcpMenuRef = useRef(null); const modeMenuRef = useRef(null); // Close dropdowns when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent | TouchEvent) => { if (mcpDropdownOpen && mcpMenuRef.current && !mcpMenuRef.current.contains(event.target as Node)) { // Check if the click was on the toggle button - if so, let the button handle it const target = event.target as Element; if (!target.closest('button[title="MCP Servers"]')) { setMcpDropdownOpen(false); } } if (modeDropdownOpen && modeMenuRef.current && !modeMenuRef.current.contains(event.target as Node)) { // Check if the click was on the toggle button const target = event.target as Element; if ( !target.closest('button[title="Analysis Modes"]') && !target.closest('.mode-selector-btn') // Add class to mode buttons to be safe ) { setModeDropdownOpen(false); } } }; document.addEventListener('mousedown', handleClickOutside); document.addEventListener('touchstart', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('touchstart', handleClickOutside); }; }, [mcpDropdownOpen, modeDropdownOpen]); return (
{/* ๐Ÿ“Ž DRAG OVERLAY - Visual feedback when dragging files */} {isDragActive && (
๐Ÿ“Ž

Drop your files here

Supports: Images, PDF, Excel, CSV, JSON, Word, PowerPoint

)} {/* Mobile Sidebar Overlay - CSS transitions instead of Framer Motion for performance */} {sidebarOpen && (
setSidebarOpen(false)} className="md:hidden fixed inset-0 z-40 bg-black/50 backdrop-blur-sm transition-opacity duration-200" /> )} {/* Sidebar - Desktop Fixed / Mobile Drawer */} {/* Main Content Area */}
{/* Top Navigation Bar - Now visible on Desktop too */}
{/* Only show title when there are messages */} {messages.length > 0 && (

Analyst Chat

{currentMode.label} Mode โ€ข {Object.values(enabledMcps).filter(Boolean).length} Active Tools

)}
{/* Top Right Actions */}
{/* Messages Container - Scrollable with ChatGPT-like experience */}
{messages.length === 0 ? ( fileInputRef.current?.click()} onPaste={handlePaste} hasAttachment={!!(selectedImage || attachedFiles.length > 0)} imagePreview={imagePreviewUrl} onRemoveImage={() => { setSelectedImage(null); setImagePreviewUrl(null); }} currentMode={currentMode} onModeClick={() => setModeDropdownOpen(true)} onMcpClick={() => setMcpDropdownOpen(true)} mcpCount={{ active: Object.values(enabledMcps).filter(Boolean).length, total: mcpServers.length }} llm={llm} setLlm={setLlm} /> ) : (
{messages.filter(msg => msg && msg.id && msg.content).map((message) => (
{message.role === 'user' ? ( // User Message - Right side, contained bubble
{editingMessageId === message.id ? (