Spaces:
Paused
Paused
| import React, { useState, useRef, useEffect } from 'react'; | |
| import { Upload, FileSpreadsheet, BarChart2, TrendingUp, Download, Settings, RefreshCw, X, AlertCircle, Brain, Sparkles } from 'lucide-react'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; | |
| import { ScrollArea } from '@/components/ui/scroll-area'; | |
| import { Badge } from '@/components/ui/badge'; | |
| import { cn } from '@/lib/utils'; | |
| import Papa from 'papaparse'; | |
| import * as d3 from 'd3'; | |
| import * as ss from 'simple-statistics'; | |
| import { API_URL } from '@/config/api'; | |
| interface DataPoint { | |
| [key: string]: string | number; | |
| } | |
| interface AnalysisResult { | |
| type: 'regression' | 'correlation' | 'clustering'; | |
| description: string; | |
| value?: number; | |
| details?: string; | |
| clusters?: number[]; | |
| } | |
| export default function DataAnalysisWidget() { | |
| const [data, setData] = useState<DataPoint[]>([]); | |
| const [columns, setColumns] = useState<string[]>([]); | |
| const [fileName, setFileName] = useState<string>(''); | |
| const [xAxis, setXAxis] = useState<string>(''); | |
| const [yAxis, setYAxis] = useState<string>(''); | |
| const [chartType, setChartType] = useState<'scatter' | 'bar' | 'line'>('scatter'); | |
| const [analysis, setAnalysis] = useState<AnalysisResult | null>(null); | |
| const [isProcessing, setIsProcessing] = useState(false); | |
| const [isAiProcessing, setIsAiProcessing] = useState(false); | |
| const d3Container = useRef<SVGSVGElement>(null); | |
| // Handle File Upload | |
| const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => { | |
| const file = event.target.files?.[0]; | |
| if (!file) return; | |
| setFileName(file.name); | |
| setIsProcessing(true); | |
| Papa.parse(file, { | |
| header: true, | |
| dynamicTyping: true, | |
| skipEmptyLines: true, | |
| complete: (results) => { | |
| const parsedData = results.data as DataPoint[]; | |
| if (parsedData.length > 0) { | |
| setData(parsedData); | |
| const cols = Object.keys(parsedData[0]); | |
| setColumns(cols); | |
| // Default selections | |
| if (cols.length >= 2) { | |
| setXAxis(cols[0]); | |
| setYAxis(cols[1]); | |
| } | |
| } | |
| setIsProcessing(false); | |
| }, | |
| error: (error) => { | |
| console.error('CSV Error:', error); | |
| setIsProcessing(false); | |
| } | |
| }); | |
| }; | |
| // Render D3 Chart | |
| useEffect(() => { | |
| if (!data.length || !d3Container.current || !xAxis || !yAxis) return; | |
| const svg = d3.select(d3Container.current); | |
| svg.selectAll('*').remove(); | |
| const margin = { top: 20, right: 20, bottom: 40, left: 50 }; | |
| const width = d3Container.current.clientWidth - margin.left - margin.right; | |
| const height = d3Container.current.clientHeight - margin.top - margin.bottom; | |
| const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`); | |
| // Scales | |
| let x: d3.AxisScale<any>; | |
| const xValues = data.map(d => d[xAxis]); | |
| if (typeof xValues[0] === 'number') { | |
| x = d3.scaleLinear() | |
| .domain([d3.min(xValues as number[]) || 0, d3.max(xValues as number[]) || 100]) | |
| .range([0, width]); | |
| } else { | |
| x = d3.scaleBand() | |
| .domain(xValues as string[]) | |
| .range([0, width]) | |
| .padding(0.1); | |
| } | |
| const y = d3.scaleLinear() | |
| .domain([0, d3.max(data, d => Number(d[yAxis])) || 100]) | |
| .range([height, 0]); | |
| // Axes | |
| g.append('g') | |
| .attr('transform', `translate(0,${height})`) | |
| .call(d3.axisBottom(x).ticks(width / 80)) // Responsive ticks | |
| .selectAll("text") | |
| .style("text-anchor", "end") | |
| .attr("dx", "-.8em") | |
| .attr("dy", ".15em") | |
| .attr("transform", "rotate(-15)"); | |
| g.append('g') | |
| .call(d3.axisLeft(y)); | |
| // Plots | |
| if (chartType === 'scatter') { | |
| g.selectAll('.dot') | |
| .data(data) | |
| .enter().append('circle') | |
| .attr('class', 'dot') | |
| .attr('cx', d => x(d[xAxis] as any) || 0) | |
| .attr('cy', d => y(Number(d[yAxis]))) | |
| .attr('r', d => (analysis?.clusters ? 6 : 4)) // Larger dots if clustered | |
| .style('fill', (d, i) => { | |
| if (analysis?.clusters) { | |
| const colors = ['#8b5cf6', '#10b981', '#f59e0b', '#ef4444', '#3b82f6']; | |
| return colors[analysis.clusters[i] % colors.length]; | |
| } | |
| return '#8b5cf6'; | |
| }) | |
| .style('opacity', 0.7); | |
| } else if (chartType === 'bar') { | |
| g.selectAll('.bar') | |
| .data(data) | |
| .enter().append('rect') | |
| .attr('class', 'bar') | |
| .attr('x', d => (x as d3.ScaleBand<string>)(String(d[xAxis])) || 0) | |
| .attr('width', (x as d3.ScaleBand<string>).bandwidth()) | |
| .attr('y', d => y(Number(d[yAxis]))) | |
| .attr('height', d => height - y(Number(d[yAxis]))) | |
| .style('fill', '#8b5cf6'); | |
| } else if (chartType === 'line') { | |
| const line = d3.line<DataPoint>() | |
| .x(d => x(d[xAxis] as any) || 0) | |
| .y(d => y(Number(d[yAxis]))) | |
| .curve(d3.curveMonotoneX); | |
| g.append("path") | |
| .datum(data) | |
| .attr("fill", "none") | |
| .attr("stroke", "#8b5cf6") | |
| .attr("stroke-width", 2) | |
| .attr("d", line); | |
| } | |
| }, [data, xAxis, yAxis, chartType, analysis]); | |
| // Basic Analysis (Client Side) | |
| const runBasicAnalysis = () => { | |
| if (!data.length || !xAxis || !yAxis) return; | |
| const xValues = data.map(d => Number(d[xAxis])); | |
| const yValues = data.map(d => Number(d[yAxis])); | |
| if (xValues.some(isNaN) || yValues.some(isNaN)) { | |
| setAnalysis({ | |
| type: 'correlation', | |
| description: 'Cannot perform regression on non-numeric data.', | |
| value: 0 | |
| }); | |
| return; | |
| } | |
| const regression = ss.linearRegression(data.map(d => [Number(d[xAxis]), Number(d[yAxis])])); | |
| const correlation = ss.sampleCorrelation(xValues, yValues); | |
| setAnalysis({ | |
| type: 'regression', | |
| description: `Linear Trend: y = ${regression.m.toFixed(2)}x + ${regression.b.toFixed(2)}`, | |
| value: correlation, | |
| details: `Correlation (r): ${correlation.toFixed(3)}` | |
| }); | |
| }; | |
| // Advanced AI Analysis (Server Side) | |
| const runAdvancedAnalysis = async () => { | |
| if (!data.length || !xAxis) return; | |
| setIsAiProcessing(true); | |
| try { | |
| // We use the new data.analysis tool via MCP | |
| const response = await fetch(`${API_URL}/api/mcp`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| tool: 'data.analysis', | |
| payload: { | |
| type: 'clustering', // Default to clustering for now | |
| data: data, | |
| params: { | |
| features: [xAxis, yAxis], | |
| k: 3 | |
| } | |
| } | |
| }) | |
| }); | |
| const result = await response.json(); | |
| if (result.error) { | |
| throw new Error(result.error); | |
| } | |
| // Handle clustering result | |
| if (result.clusters) { | |
| setAnalysis({ | |
| type: 'clustering', | |
| description: `AI identified 3 distinct clusters based on ${xAxis} and ${yAxis}.`, | |
| clusters: result.clusters, | |
| details: 'Visualization updated with cluster colors.' | |
| }); | |
| } | |
| } catch (error: any) { | |
| console.error('AI Analysis Failed:', error); | |
| setAnalysis({ | |
| type: 'correlation', | |
| description: 'AI Analysis Failed', | |
| details: error.message, | |
| value: 0 | |
| }); | |
| } finally { | |
| setIsAiProcessing(false); | |
| } | |
| }; | |
| return ( | |
| <div className="h-full flex flex-col bg-card/80 backdrop-blur-sm border border-border/50 rounded-lg overflow-hidden shadow-lg"> | |
| {/* Header */} | |
| <div className="p-4 border-b border-border/50 bg-gradient-to-r from-emerald-500/10 to-teal-500/5 flex items-center justify-between"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2 bg-gradient-to-br from-emerald-500 to-teal-600 rounded-lg shadow-md"> | |
| <BarChart2 className="w-5 h-5 text-white" /> | |
| </div> | |
| <div> | |
| <h2 className="font-bold text-foreground tracking-tight">AI Data Analyst</h2> | |
| <div className="flex items-center gap-2 text-[10px] font-mono text-muted-foreground"> | |
| <span className={cn("w-1.5 h-1.5 rounded-full", data.length > 0 ? "bg-green-400" : "bg-gray-400")} /> | |
| {data.length > 0 ? `${data.length} ROWS LOADED` : "READY FOR DATA"} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex gap-2"> | |
| <div className="relative"> | |
| <input | |
| type="file" | |
| accept=".csv,.json" | |
| onChange={handleFileUpload} | |
| className="absolute inset-0 opacity-0 cursor-pointer" | |
| /> | |
| <Button size="sm" variant="outline" className="h-8 gap-2"> | |
| <Upload size={14} /> Import Data | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Main Content */} | |
| <div className="flex-1 flex overflow-hidden"> | |
| {/* Controls Sidebar */} | |
| <div className="w-64 border-r border-border/50 bg-secondary/10 p-4 flex flex-col gap-6"> | |
| {data.length > 0 ? ( | |
| <> | |
| <div className="space-y-3"> | |
| <label className="text-xs font-semibold text-muted-foreground uppercase">Axes Configuration</label> | |
| <div className="space-y-2"> | |
| <div className="space-y-1"> | |
| <span className="text-[10px] text-muted-foreground">X Axis (Independent)</span> | |
| <Select value={xAxis} onValueChange={setXAxis}> | |
| <SelectTrigger><SelectValue /></SelectTrigger> | |
| <SelectContent> | |
| {columns.map(c => <SelectItem key={c} value={c}>{c}</SelectItem>)} | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| <div className="space-y-1"> | |
| <span className="text-[10px] text-muted-foreground">Y Axis (Dependent)</span> | |
| <Select value={yAxis} onValueChange={setYAxis}> | |
| <SelectTrigger><SelectValue /></SelectTrigger> | |
| <SelectContent> | |
| {columns.map(c => <SelectItem key={c} value={c}>{c}</SelectItem>)} | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="space-y-3"> | |
| <label className="text-xs font-semibold text-muted-foreground uppercase">Visualization</label> | |
| <Select value={chartType} onValueChange={(v: any) => setChartType(v)}> | |
| <SelectTrigger><SelectValue /></SelectTrigger> | |
| <SelectContent> | |
| <SelectItem value="scatter">Scatter Plot</SelectItem> | |
| <SelectItem value="line">Line Chart</SelectItem> | |
| <SelectItem value="bar">Bar Chart</SelectItem> | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| <div className="space-y-2"> | |
| <Button onClick={runBasicAnalysis} variant="secondary" className="w-full"> | |
| <TrendingUp className="w-4 h-4 mr-2" /> Basic Trend | |
| </Button> | |
| <Button | |
| onClick={runAdvancedAnalysis} | |
| className="w-full bg-emerald-600 hover:bg-emerald-700 relative overflow-hidden" | |
| disabled={isAiProcessing} | |
| > | |
| {isAiProcessing && ( | |
| <div className="absolute inset-0 bg-white/20 animate-pulse" /> | |
| )} | |
| <Brain className="w-4 h-4 mr-2" /> | |
| {isAiProcessing ? 'Thinking...' : 'AI Clustering'} | |
| </Button> | |
| </div> | |
| {analysis && ( | |
| <div className={cn( | |
| "p-3 border rounded-md text-sm", | |
| analysis.type === 'clustering' ? "bg-purple-500/10 border-purple-500/20" : "bg-emerald-500/10 border-emerald-500/20" | |
| )}> | |
| <h4 className={cn( | |
| "font-bold mb-1 flex items-center gap-1", | |
| analysis.type === 'clustering' ? "text-purple-500" : "text-emerald-500" | |
| )}> | |
| <Sparkles className="w-3 h-3" /> | |
| {analysis.type === 'clustering' ? 'AI Insight' : 'Basic Insight'} | |
| </h4> | |
| <p className="font-medium">{analysis.description}</p> | |
| {analysis.details && <p className="text-xs text-muted-foreground mt-1">{analysis.details}</p>} | |
| </div> | |
| )} | |
| </> | |
| ) : ( | |
| <div className="text-center text-muted-foreground text-sm mt-10"> | |
| <FileSpreadsheet className="w-10 h-10 mx-auto mb-3 opacity-20" /> | |
| <p>Upload a CSV file to begin analysis</p> | |
| </div> | |
| )} | |
| </div> | |
| {/* Chart Area */} | |
| <div className="flex-1 bg-background/50 relative p-4"> | |
| {data.length > 0 ? ( | |
| <svg | |
| ref={d3Container} | |
| className="w-full h-full overflow-visible" | |
| style={{ minHeight: '300px' }} | |
| /> | |
| ) : ( | |
| <div className="absolute inset-0 flex items-center justify-center text-muted-foreground/20"> | |
| <BarChart2 className="w-32 h-32" /> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } |