import React, { useState, useMemo } from 'react'; import { InvestigationCase } from '../../types/forensics'; import { RelationalTable, QueryResult, PivotConfig, PivotResult } from '../../types/database'; import { SqlEngine, computePivotTable } from '../../services/sqlEngine'; import { generateRelationalDatasets } from '../../services/relationalDataGenerator'; import { Database, Table as TableIcon, Play, Download, Copy, Check, Layers, Sliders, Search, FileSpreadsheet, Key, Code, Sparkles, RefreshCw, Clock, ArrowUpDown, Filter, BarChart2, ChevronRight, ExternalLink } from 'lucide-react'; interface WorkbookDatabaseTabProps { allCases: InvestigationCase[]; currentCase: InvestigationCase; initialQuery?: string; } const PRESET_SQL_QUERIES = [ { name: 'BSA Structuring Smurf Detection ($8k-$10k)', sql: `SELECT tx_id, source_node, target_node, amount_usd, asset, hop_type, risk_tag FROM transactions_fact WHERE amount_usd BETWEEN 8000 AND 9999 ORDER BY amount_usd DESC;`, }, { name: 'Upstream Risk Concentration by Jurisdiction', sql: `SELECT n.jurisdiction, COUNT(t.tx_id) AS tx_count, SUM(t.amount_usd) AS total_volume_usd, AVG(n.risk_score) AS avg_risk_score FROM nodes_dimension n JOIN transactions_fact t ON n.node_id = t.source_node GROUP BY n.jurisdiction ORDER BY total_volume_usd DESC;`, }, { name: 'Speed-of-Light & STUN Leak Physical Anomalies', sql: `SELECT case_id, claimed_ip, claimed_country, unmasked_real_ip, propagation_delta_ms, ja4_fingerprint, detected_client_engine FROM telemetry_audit WHERE speed_of_light_violation = true OR webrtc_leak_detected = true ORDER BY propagation_delta_ms DESC;`, }, { name: 'High-Volume Intermediary Sinks & Mixers by Depth', sql: `SELECT node_id, label, entity_type, jurisdiction, upstream_depth, total_inflow_usd, in_degree, out_degree, risk_score FROM nodes_dimension WHERE total_inflow_usd > 10000 ORDER BY upstream_depth ASC, total_inflow_usd DESC;`, }, { name: 'Cross-Tabulated Liquidity Flows by Asset & Hop', sql: `SELECT asset, hop_type, COUNT(*) AS tx_count, SUM(amount_usd) AS total_volume_usd, AVG(amount_usd) AS avg_ticket_usd FROM transactions_fact GROUP BY asset, hop_type ORDER BY total_volume_usd DESC;`, }, { name: 'Academic AML Hypothesis Test Summary', sql: `SELECT metric_id, metric_name, test_statistic, p_value, critical_threshold, verdict FROM statistical_metrics ORDER BY test_statistic DESC;`, }, ]; export const WorkbookDatabaseTab: React.FC = ({ allCases, currentCase, initialQuery, }) => { const [activeViewMode, setActiveViewMode] = useState<'SQL_STUDIO' | 'PIVOT_STUDIO' | 'TABLE_EXPLORER'>('SQL_STUDIO'); // Build Relational Database const { tables: initialTables } = useMemo(() => { return generateRelationalDatasets(allCases, currentCase); }, [allCases, currentCase]); const [customTables, setCustomTables] = useState([]); const allTables = useMemo(() => [...initialTables, ...customTables], [initialTables, customTables]); const sqlEngine = useMemo(() => new SqlEngine(allTables), [allTables]); // SQL Studio State const [sqlQuery, setSqlQuery] = useState( initialQuery || PRESET_SQL_QUERIES[0].sql ); const [queryResult, setQueryResult] = useState(() => { return sqlEngine.execute(initialQuery || PRESET_SQL_QUERIES[0].sql); }); const [copied, setCopied] = useState(false); const [resultFilter, setResultFilter] = useState(''); const [newViewName, setNewViewName] = useState(''); const [isCreatingView, setIsCreatingView] = useState(false); // Pivot Studio State const [pivotTableSelection, setPivotTableSelection] = useState('transactions_fact'); const [pivotConfig, setPivotConfig] = useState({ rowField: 'hop_type', colField: 'asset', valField: 'amount_usd', aggFunc: 'SUM', }); // Table Explorer State const [selectedExplorerTable, setSelectedExplorerTable] = useState('nodes_dimension'); // Execute SQL const handleExecuteSql = (queryToRun?: string) => { const q = queryToRun || sqlQuery; const res = sqlEngine.execute(q); setQueryResult(res); }; // Create materialized custom table from SQL query const handleCreateView = () => { if (!newViewName.trim() || queryResult.rows.length === 0) return; const safeName = newViewName.trim().replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase(); const newTable: RelationalTable = { id: `view_${Date.now()}`, name: safeName, displayName: `Custom View: ${safeName}`, description: `Materialized view from query result (${queryResult.rowCount} rows)`, rowCount: queryResult.rowCount, columns: queryResult.columns.map(col => ({ name: col, type: typeof queryResult.rows[0]?.[col] === 'number' ? 'number' : typeof queryResult.rows[0]?.[col] === 'boolean' ? 'boolean' : 'string', description: `Derived column ${col}`, })), rows: queryResult.rows, }; setCustomTables(prev => [...prev.filter(t => t.name !== safeName), newTable]); setIsCreatingView(false); setNewViewName(''); }; // Export functions const exportToCsv = (rows: Record[], columns: string[], filename: string) => { if (rows.length === 0) return; const header = columns.join(','); const body = rows.map(r => columns.map(c => JSON.stringify(r[c] ?? '')).join(',')).join('\n'); const csvContent = `data:text/csv;charset=utf-8,${encodeURIComponent(header + '\n' + body)}`; const link = document.createElement('a'); link.setAttribute('href', csvContent); link.setAttribute('download', `${filename}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); }; const exportToJson = (rows: Record[], filename: string) => { const jsonContent = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(rows, null, 2))}`; const link = document.createElement('a'); link.setAttribute('href', jsonContent); link.setAttribute('download', `${filename}.json`); document.body.appendChild(link); link.click(); document.body.removeChild(link); }; const copyAsGoogleSheetsTsv = (rows: Record[], columns: string[]) => { if (rows.length === 0) return; const header = columns.join('\t'); const body = rows.map(r => columns.map(c => String(r[c] ?? '')).join('\t')).join('\n'); navigator.clipboard.writeText(header + '\n' + body); setCopied(true); setTimeout(() => setCopied(false), 2000); }; // Compute Pivot Result const activePivotTable = allTables.find(t => t.name === pivotTableSelection) || allTables[0]; const pivotResult: PivotResult = useMemo(() => { if (!activePivotTable) { return { rowKeys: [], colKeys: [], matrix: {}, rowTotals: {}, colTotals: {}, grandTotal: 0, totalRecords: 0, maxVal: 0, minVal: 0, }; } return computePivotTable(activePivotTable.rows, pivotConfig); }, [activePivotTable, pivotConfig]); // Filtered Query Result Rows const filteredResultRows = useMemo(() => { if (!resultFilter.trim()) return queryResult.rows; const lower = resultFilter.toLowerCase(); return queryResult.rows.filter(r => Object.values(r).some(v => String(v).toLowerCase().includes(lower)) ); }, [queryResult.rows, resultFilter]); const explorerTable = allTables.find(t => t.name === selectedExplorerTable) || allTables[0]; return (
{/* Top Banner: Relational Analytics Workbook & SQL Studio */}

Relational Analytics Workbook & SQL Pivot Database

{allTables.length} RELATIONAL TABLES • IN-MEMORY ENGINE

Perform SQL queries, relational joins, cross-tabulations, and custom visual pivot tables directly on forensic telemetry and multi-hop ledgers.

{/* View Mode Tabs */}
{/* ========================================================================= */} {/* MODE 1: INTERACTIVE SQL STUDIO */} {/* ========================================================================= */} {activeViewMode === 'SQL_STUDIO' && (
{/* Schema Explorer & Pre-baked Forensic Templates (1 Col) */}
{/* Relational Schema Browser */}
RELATIONAL SCHEMA
{allTables.length} Tables
{allTables.map(t => (
{t.name} {t.rowCount} rows
{t.columns.slice(0, 5).map(c => ( setSqlQuery(prev => prev + ` ${c.name}`)} className="bg-slate-900 hover:bg-slate-800 text-slate-400 hover:text-cyan-200 px-1 py-0.5 rounded text-[9px] cursor-pointer" title={`${c.name} (${c.type}): ${c.description}`} > {c.primaryKey && } {c.name} ))} {t.columns.length > 5 && ( +{t.columns.length - 5} more )}
))}
{/* Forensic SQL Templates */}
FORENSIC SQL PRESETS
{PRESET_SQL_QUERIES.map(preset => ( ))}
{/* SQL Editor & Interactive Query Results Grid (3 Cols) */}
{/* SQL Query Editor Card */}
SQL QUERY EDITOR
{/* View Name Materialization Prompt */} {isCreatingView && (
setNewViewName(e.target.value)} className="flex-1 bg-slate-900 border border-slate-700 text-slate-200 px-2.5 py-1 text-xs rounded focus:outline-none focus:border-cyan-500 font-mono" />
)} {/* Textarea Code Area */}