Spaces:
Sleeping
Sleeping
| import { useState, useCallback } from 'react'; | |
| import { Upload, FileSpreadsheet, AlertTriangle } from 'lucide-react'; | |
| import { api } from '../../api/client'; | |
| import { useStore } from '../../store/useStore'; | |
| import { Panel } from '../common/Panel'; | |
| import { LoadingSpinner } from '../common/LoadingSpinner'; | |
| import { DataTable } from './DataTable'; | |
| import { FilterPanel } from './FilterPanel'; | |
| import type { ColumnStat, DataResponse } from '../../types'; | |
| export function DataExplorer() { | |
| const { data, columnStats, setData, dataLoaded } = useStore(); | |
| const [loading, setLoading] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const [activeData, setActiveData] = useState<DataResponse | null>(null); | |
| const [activeStats, setActiveStats] = useState<ColumnStat[]>([]); | |
| const displayData = activeData ?? data; | |
| const displayStats = activeStats.length > 0 ? activeStats : columnStats; | |
| const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { | |
| const file = e.target.files?.[0]; | |
| if (!file) return; | |
| setLoading(true); | |
| setError(null); | |
| try { | |
| const res = await api.uploadFile(file); | |
| setData(res.data, res.column_stats); | |
| setActiveData(null); | |
| setActiveStats([]); | |
| } catch (err: unknown) { | |
| setError(err instanceof Error ? err.message : 'Upload failed'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| const handleFilter = useCallback( | |
| async ( | |
| filters: { | |
| column: string; | |
| type: string; | |
| values?: string[]; | |
| min_val?: number; | |
| max_val?: number; | |
| pattern?: string; | |
| }[] | |
| ) => { | |
| if (filters.length === 0) { | |
| setActiveData(null); | |
| setActiveStats([]); | |
| setError(null); | |
| return; | |
| } | |
| setLoading(true); | |
| setError(null); | |
| try { | |
| const res = await api.filterData(filters); | |
| setActiveData(res.data); | |
| setActiveStats(res.column_stats); | |
| } catch (err: unknown) { | |
| setError(err instanceof Error ? err.message : 'Filtering failed'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }, | |
| [] | |
| ); | |
| return ( | |
| <div className="space-y-4"> | |
| <div className="flex flex-wrap items-center gap-3"> | |
| <label className="flex cursor-pointer items-center gap-2 rounded-md border border-border bg-surface px-4 py-2 text-sm text-text-primary transition-colors hover:border-purple hover:bg-elevated"> | |
| <Upload className="h-4 w-4 text-purple" /> | |
| Upload Files | |
| <input type="file" accept=".csv,.xlsx,.xls,.json" onChange={handleUpload} className="hidden" /> | |
| </label> | |
| {dataLoaded && displayData && ( | |
| <div className="ml-auto flex items-center gap-3 text-xs text-text-muted"> | |
| <FileSpreadsheet className="h-4 w-4" /> | |
| <span> | |
| {displayData.returned_rows.toLocaleString()} / {displayData.total_rows.toLocaleString()} rows | |
| </span> | |
| <span>{displayData.columns.length} columns</span> | |
| </div> | |
| )} | |
| </div> | |
| {error && ( | |
| <div className="flex items-center gap-2 rounded-md border border-danger/30 bg-danger/10 px-4 py-2.5 text-sm text-danger"> | |
| <AlertTriangle className="h-4 w-4" /> {error} | |
| </div> | |
| )} | |
| {loading && <LoadingSpinner text="Processing data..." />} | |
| {/* Filters */} | |
| {dataLoaded && displayStats.length > 0 && ( | |
| <FilterPanel columns={displayStats} onApply={handleFilter} /> | |
| )} | |
| {/* Column Stats */} | |
| {dataLoaded && displayStats.length > 0 && ( | |
| <Panel title="Column Statistics" subtitle={`${displayStats.length} columns`}> | |
| <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6"> | |
| {displayStats.map((col) => ( | |
| <div | |
| key={col.name} | |
| className="rounded border border-border/50 bg-raised p-2.5" | |
| > | |
| <p className="truncate text-xs font-medium text-text-primary" title={col.name}> | |
| {col.name} | |
| </p> | |
| <p className="mt-0.5 text-[10px] text-text-muted"> | |
| {col.dtype} | {col.unique} unique | |
| </p> | |
| {col.top_values && col.top_values.length > 0 && ( | |
| <div className="mt-1.5 space-y-0.5"> | |
| {col.top_values.slice(0, 3).map((tv) => ( | |
| <div key={tv.value} className="flex items-center gap-1"> | |
| <div | |
| className="h-1 rounded-full bg-accent/60" | |
| style={{ | |
| width: `${Math.min(100, (tv.count / (col.non_null || 1)) * 100)}%`, | |
| }} | |
| /> | |
| <span className="whitespace-nowrap text-[9px] text-text-muted"> | |
| {tv.value.slice(0, 15)} | |
| </span> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| </Panel> | |
| )} | |
| {/* Data Table */} | |
| {dataLoaded && displayData && ( | |
| <Panel title="Data View" noPad> | |
| <DataTable data={displayData} maxHeight="calc(100vh - 420px)" /> | |
| </Panel> | |
| )} | |
| </div> | |
| ); | |
| } | |