import { useState, useCallback } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import Editor from '@monaco-editor/react'; import { apiGet, apiPost, apiDelete } from '../api/client'; import { LoadingSpinner, Card } from '../components/ui'; import { BarChart, Bar, LineChart, Line, ScatterChart, Scatter, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, } from 'recharts'; import { ChevronDown, ChevronRight, Key, Link, Search } from 'lucide-react'; interface QueryResult { columns: Array<{ name: string; type: string }>; rows: unknown[][]; row_count: number; elapsed_ms: number; } interface SavedQuery { id: string; name: string; description: string; sql_text: string; created_at: string; } interface SchemaColumn { name: string; type: string; nullable?: boolean; primary_key?: boolean; references?: string; has_default?: boolean; } interface SchemaTable { name: string; row_estimate?: number; columns: SchemaColumn[]; } interface SchemaInfo { catalog: string; schema: string; tables: SchemaTable[]; } type ChartType = 'none' | 'auto' | 'bar' | 'line' | 'scatter'; export function SqlExplorerPage() { const qc = useQueryClient(); const [sql, setSql] = useState('SELECT 1 AS test'); const [result, setResult] = useState(null); const [error, setError] = useState(null); const [chartType, setChartType] = useState('none'); const [xCol, setXCol] = useState(0); const [yCol, setYCol] = useState(1); const [schemaFilter, setSchemaFilter] = useState(''); const [expandedTables, setExpandedTables] = useState>(new Set()); const { data: schema } = useQuery({ queryKey: ['pg-schema'], queryFn: () => apiGet('query', '/api/analytics/pg-schema'), }); const { data: savedQueries } = useQuery({ queryKey: ['saved-queries'], queryFn: () => apiGet('query', '/api/analytics/saved-queries'), }); const executeMutation = useMutation({ mutationFn: (sqlText: string) => apiPost('query', '/api/analytics/pg-query', { sql: sqlText, limit: 1000 }), onSuccess: (data) => { setResult(data); setError(null); }, onError: (err: Error) => { const detail = (err as { body?: { detail?: string } }).body?.detail; setError(detail || err.message); setResult(null); }, }); const saveMutation = useMutation({ mutationFn: (body: { name: string; sql_text: string }) => apiPost('query', '/api/analytics/saved-queries', body), onSuccess: () => qc.invalidateQueries({ queryKey: ['saved-queries'] }), }); const deleteMutation = useMutation({ mutationFn: (id: string) => apiDelete('query', `/api/analytics/saved-queries/${id}`), onSuccess: () => qc.invalidateQueries({ queryKey: ['saved-queries'] }), }); const handleExecute = useCallback(() => { if (sql.trim()) executeMutation.mutate(sql); }, [sql, executeMutation]); const handleSave = useCallback(() => { const name = prompt('Query name:'); if (name) saveMutation.mutate({ name, sql_text: sql }); }, [sql, saveMutation]); const handleQuickInsert = useCallback((tableName: string) => { const query = `SELECT * FROM ${tableName} LIMIT 100`; setSql(query); }, []); const toggleTable = useCallback((name: string) => { setExpandedTables((prev) => { const next = new Set(prev); if (next.has(name)) next.delete(name); else next.add(name); return next; }); }, []); const formatRowCount = (n: number) => { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`; return String(n); }; const filteredTables = (schema?.tables ?? []).filter((t) => { if (!schemaFilter) return true; const q = schemaFilter.toLowerCase(); return t.name.toLowerCase().includes(q) || t.columns.some((c) => c.name.toLowerCase().includes(q)); }); // Build chart data from result // Auto-detect best chart type and columns when chartType === 'auto' const autoChart = (() => { if (!result || result.columns.length < 2 || result.row_count === 0) { return { type: 'bar' as const, x: 0, y: 1 }; } const cols = result.columns; const rows = result.rows; // Classify each column as numeric or categorical const isNumeric = cols.map((_, ci) => { const sample = rows.slice(0, 10); const numCount = sample.filter((r) => { const v = r[ci]; return v != null && !isNaN(parseFloat(String(v))) && String(v).trim() !== ''; }).length; return numCount > sample.length * 0.7; }); // Check if a column looks like a date/time const isDateLike = cols.map((c, ci) => { const name = c.name.toLowerCase(); if (name.includes('date') || name.includes('time') || name.includes('_at') || name.includes('created') || name.includes('generated')) return true; const sample = String(rows[0]?.[ci] ?? ''); return /^\d{4}-\d{2}/.test(sample); }); const numericCols = cols.map((_, i) => i).filter((i) => isNumeric[i]); const categoricalCols = cols.map((_, i) => i).filter((i) => !isNumeric[i]); const dateCols = cols.map((_, i) => i).filter((i) => isDateLike[i]); // Heuristics: // 1. Date column + numeric → line chart (time series) // 2. Categorical + numeric → bar chart (categories) // 3. Two numeric columns → scatter plot // 4. Fallback → bar chart with first categorical as X, first numeric as Y if (dateCols.length > 0 && numericCols.length > 0) { const xIdx = dateCols[0]; const yIdx = numericCols.find((i) => i !== xIdx) ?? numericCols[0]; return { type: 'line' as const, x: xIdx, y: yIdx }; } if (categoricalCols.length > 0 && numericCols.length > 0) { return { type: 'bar' as const, x: categoricalCols[0], y: numericCols[0] }; } if (numericCols.length >= 2) { return { type: 'scatter' as const, x: numericCols[0], y: numericCols[1] }; } // Fallback: first col as X, second as Y, bar chart return { type: 'bar' as const, x: 0, y: Math.min(1, cols.length - 1) }; })(); // Resolve effective chart settings (auto overrides manual selections) const effectiveChartType = chartType === 'auto' ? autoChart.type : chartType; const effectiveXCol = chartType === 'auto' ? autoChart.x : xCol; const effectiveYCol = chartType === 'auto' ? autoChart.y : yCol; const chartData = result && result.columns.length >= 2 ? result.rows.map((row) => { const yRaw = row[effectiveYCol]; const yNum = yRaw != null ? parseFloat(String(yRaw)) : NaN; const entry: Record = { x: row[effectiveXCol], y: isNaN(yNum) ? 0 : yNum, label: String(row[effectiveXCol] ?? ''), }; // Attach all column values for rich tooltips for (let i = 0; i < result.columns.length; i++) { entry[`_col${i}`] = row[i]; } return entry; }) : []; // Custom tooltip that shows all row values on hover const renderTooltip = ({ active, payload }: Record) => { if (!active || !result) return null; const items = payload as Array<{ payload: Record }> | undefined; if (!items?.length) return null; const data = items[0].payload; return (
{result.columns.map((col, i) => { const val = data[`_col${i}`]; const isY = i === effectiveYCol; const isX = i === effectiveXCol; return (
{col.name}: {val == null ? 'NULL' : String(val)}
); })}
); }; // Split saved queries into pre-built (no id-based delete) and user-saved const preBuiltNames = new Set([ 'Company Overview', 'Recent Recommendations', 'High Confidence Buys', 'Trend Windows Summary', 'Market Prices', 'Document Counts by Type', 'Global Events', 'Trading Decisions', 'Ingestion Health', 'Reserve Pool Ledger', 'Sector Exposure', 'Source Coverage Matrix', ]); const preBuiltQueries = (savedQueries ?? []).filter((sq) => preBuiltNames.has(sq.name)); const userQueries = (savedQueries ?? []).filter((sq) => !preBuiltNames.has(sq.name)); return (
{/* Schema browser sidebar */}

Schema

setSchemaFilter(e.target.value)} placeholder="Filter tables…" className="w-full rounded border border-surface-700 bg-surface-800 py-1 pl-6 pr-2 text-xs text-gray-300 placeholder-gray-600 focus:border-brand-500 focus:outline-none" />

{schema?.tables.length ?? 0} tables

{filteredTables.map((t) => { const isExpanded = expandedTables.has(t.name); return (
{t.row_estimate != null && ( {formatRowCount(t.row_estimate)} )}
{isExpanded && (
{t.columns.map((c) => (
{c.primary_key && } {c.references && } {c.name} {c.type}
))}
)}
); })} {/* Pre-built queries */} {preBuiltQueries.length > 0 && ( <>

Pre-built Queries

{preBuiltQueries.map((sq) => (
{sq.description && (

{sq.description}

)}
))} )} {/* User saved queries */}

Saved Queries

{userQueries.length === 0 && (

No saved queries yet

)} {userQueries.map((sq) => (
))}
{/* Main area */}
{/* Editor */}
setSql(v ?? '')} theme="vs-dark" options={{ minimap: { enabled: false }, fontSize: 13, lineNumbers: 'on', scrollBeyondLastLine: false }} />
{/* Controls */}
{result && ( {result.row_count} rows in {result.elapsed_ms}ms )} {error && {error}}
{/* Results */} {executeMutation.isPending && } {result && (
{result.columns.map((col, i) => ( ))} {result.rows.map((row, ri) => ( {row.map((cell, ci) => ( ))} ))}
{col.name} ({col.type})
{cell == null ? NULL : String(cell)}
)} {/* Chart builder */} {result && result.columns.length >= 2 && (
Chart: {(['none', 'auto', 'bar', 'line', 'scatter'] as ChartType[]).map((ct) => ( ))} {chartType !== 'none' && chartType !== 'auto' && ( <> )} {chartType === 'auto' && result && ( {autoChart.type} · X: {result.columns[autoChart.x]?.name} · Y: {result.columns[autoChart.y]?.name} )}
{chartType !== 'none' && chartData.length > 0 && ( {effectiveChartType === 'bar' ? ( ) : effectiveChartType === 'line' ? ( ) : ( )} )}
)}
); }