Kraft102's picture
fix: remove undefined width/height from useEffect dependency array
c1a8055
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>
);
}