'use client'; /** * SQL Console Component * * Allows executing SQL queries against the memory database. * Read-only by default with optional write mode. */ import { useState, useRef, useEffect } from 'react'; import { Button } from '@/components/ui/button'; const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; // Predefined query templates const QUERY_TEMPLATES = [ { label: 'Top memories by salience', query: 'SELECT id, title, salience, type, category FROM memories ORDER BY salience DESC LIMIT 20', }, { label: 'Memory type distribution', query: "SELECT type, COUNT(*) as count FROM memories GROUP BY type", }, { label: 'Category distribution', query: 'SELECT category, COUNT(*) as count FROM memories GROUP BY category ORDER BY count DESC', }, { label: 'Contradiction links', query: "SELECT * FROM memory_links WHERE relationship = 'contradicts'", }, { label: 'Recently accessed', query: 'SELECT id, title, last_accessed, access_count FROM memories ORDER BY last_accessed DESC LIMIT 20', }, { label: 'Low salience (at risk)', query: 'SELECT id, title, salience, decayed_score, type FROM memories WHERE decayed_score < 0.3 ORDER BY decayed_score ASC LIMIT 20', }, { label: 'Memories by project', query: 'SELECT project, COUNT(*) as count FROM memories GROUP BY project ORDER BY count DESC', }, ]; interface QueryResult { columns: string[]; rows: Record[]; rowCount: number; executionTime: number; error?: string; } export function SqlConsole() { const [query, setQuery] = useState(QUERY_TEMPLATES[0].query); const [result, setResult] = useState(null); const [isExecuting, setIsExecuting] = useState(false); const [allowWrite, setAllowWrite] = useState(false); const [history, setHistory] = useState([]); const textareaRef = useRef(null); // Keyboard shortcut: Ctrl+Enter to execute const executeQueryRef = useRef<() => void>(() => {}); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); executeQueryRef.current(); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, []); const executeQuery = async () => { if (!query.trim()) return; // Safety check for destructive operations const upperQuery = query.toUpperCase(); if (!allowWrite) { if (upperQuery.includes('DROP') || upperQuery.includes('DELETE') || upperQuery.includes('TRUNCATE') || upperQuery.includes('INSERT') || upperQuery.includes('UPDATE') || upperQuery.includes('ALTER')) { setResult({ columns: [], rows: [], rowCount: 0, executionTime: 0, error: 'Write operations are disabled. Enable "Allow writes" to execute this query.', }); return; } } // Block DROP/TRUNCATE even in write mode if (upperQuery.includes('DROP') || upperQuery.includes('TRUNCATE')) { setResult({ columns: [], rows: [], rowCount: 0, executionTime: 0, error: 'DROP and TRUNCATE operations are blocked for safety.', }); return; } setIsExecuting(true); const startTime = performance.now(); try { const response = await fetch(`${API_BASE}/api/sql`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: query.trim(), allowWrite }), }); const data = await response.json(); const executionTime = performance.now() - startTime; if (!response.ok) { setResult({ columns: [], rows: [], rowCount: 0, executionTime, error: data.error || 'Query failed', }); } else { setResult({ columns: data.columns || [], rows: data.rows || [], rowCount: data.rowCount || data.rows?.length || 0, executionTime, }); // Add to history setHistory((prev) => { const updated = [query, ...prev.filter((q) => q !== query)]; return updated.slice(0, 20); // Keep last 20 }); } } catch (err) { setResult({ columns: [], rows: [], rowCount: 0, executionTime: performance.now() - startTime, error: (err as Error).message, }); } finally { setIsExecuting(false); } }; executeQueryRef.current = executeQuery; const loadTemplate = (templateQuery: string) => { setQuery(templateQuery); textareaRef.current?.focus(); }; const loadFromHistory = (historicalQuery: string) => { setQuery(historicalQuery); textareaRef.current?.focus(); }; return (
{/* Query Editor */}
{/* Template Dropdown */} {/* History Dropdown */} {history.length > 0 && ( )}
{/* Allow Write Toggle */}