ar9avg's picture
Initial submission: SQL Agent OpenEnv for Meta+HF hackathon
3c665d2
import { useState, useEffect, useRef } from 'react'
import { Loader2, GitFork } from 'lucide-react'
import { useStore } from '../store/useStore'
import { fetchSchemaGraph } from '../lib/api'
import type { SchemaTable, SchemaRelationship } from '../lib/types'
// ─── Table card ───────────────────────────────────────────────────
function TableCard({ table, x, y }: { table: SchemaTable; x: number; y: number }) {
return (
<g transform={`translate(${x},${y})`}>
{/* Card bg */}
<rect
width={180}
height={28 + table.columns.length * 20}
rx={8}
fill="#0e0e16"
stroke="rgba(255,255,255,0.08)"
strokeWidth={1}
/>
{/* Header */}
<rect width={180} height={28} rx={8} fill="rgba(139,92,246,0.15)" />
<rect y={20} width={180} height={8} fill="rgba(139,92,246,0.15)" />
<text
x={10}
y={18}
fill="#a78bfa"
fontSize={11}
fontWeight="bold"
fontFamily="ui-monospace,monospace"
>
{table.name}
</text>
{/* Columns */}
{table.columns.map((col, i) => (
<g key={col.name} transform={`translate(0,${28 + i * 20})`}>
<rect
width={180}
height={20}
fill={i % 2 === 0 ? 'rgba(255,255,255,0.01)' : 'transparent'}
/>
<text
x={10}
y={14}
fill={col.pk ? '#60a5fa' : col.fk ? '#34d399' : 'rgba(255,255,255,0.5)'}
fontSize={10}
fontFamily="ui-monospace,monospace"
>
{col.pk ? 'πŸ”‘ ' : col.fk ? 'πŸ”— ' : ' '}
{col.name}
</text>
<text
x={170}
y={14}
fill="rgba(255,255,255,0.2)"
fontSize={9}
fontFamily="ui-monospace,monospace"
textAnchor="end"
>
{col.type}
</text>
</g>
))}
</g>
)
}
// ─── Layout helpers ───────────────────────────────────────────────
function layoutTables(tables: SchemaTable[]) {
const CARD_W = 180
const CARD_H_BASE = 28
const COL_H = 20
const GAP_X = 40
const GAP_Y = 30
const COLS_PER_ROW = 3
const positions: Record<string, { x: number; y: number; w: number; h: number }> = {}
let maxRowH = 0
tables.forEach((t, i) => {
const col = i % COLS_PER_ROW
const row = Math.floor(i / COLS_PER_ROW)
const h = CARD_H_BASE + t.columns.length * COL_H
if (row === Math.floor(i / COLS_PER_ROW) && col === 0) maxRowH = 0
maxRowH = Math.max(maxRowH, h)
const prevRowsH = tables
.slice(0, row * COLS_PER_ROW)
.reduce((acc, _, idx) => {
if (idx % COLS_PER_ROW === 0) {
const rowH = tables.slice(idx, idx + COLS_PER_ROW).reduce(
(m, rt) => Math.max(m, CARD_H_BASE + rt.columns.length * COL_H),
0
)
return acc + rowH + GAP_Y
}
return acc
}, 0)
positions[t.name] = {
x: col * (CARD_W + GAP_X) + 20,
y: prevRowsH + 20,
w: CARD_W,
h,
}
})
return positions
}
function RelationshipLine({
from,
to,
positions,
}: {
from: string
to: string
positions: Record<string, { x: number; y: number; w: number; h: number }>
}) {
const a = positions[from]
const b = positions[to]
if (!a || !b) return null
const x1 = a.x + a.w
const y1 = a.y + 14
const x2 = b.x
const y2 = b.y + 14
const cx = (x1 + x2) / 2
return (
<path
d={`M${x1},${y1} C${cx},${y1} ${cx},${y2} ${x2},${y2}`}
stroke="rgba(139,92,246,0.3)"
strokeWidth={1.5}
fill="none"
strokeDasharray="4 3"
/>
)
}
// ─── ER Diagram component ─────────────────────────────────────────
export function ERDiagram() {
const { schemaGraph, setSchemaGraph } = useStore()
const [loading, setLoading] = useState(false)
const svgRef = useRef<SVGSVGElement>(null)
const load = async () => {
setLoading(true)
try {
const data = await fetchSchemaGraph()
setSchemaGraph(data)
} catch {
// noop
} finally {
setLoading(false)
}
}
useEffect(() => {
if (!schemaGraph) void load()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
if (loading) {
return (
<div className="flex items-center justify-center h-full gap-2 text-gray-500">
<Loader2 size={16} className="animate-spin" />
<span className="text-sm">Loading schema...</span>
</div>
)
}
if (!schemaGraph || schemaGraph.tables.length === 0) {
return (
<div className="flex flex-col items-center justify-center h-full gap-3 text-gray-600">
<GitFork size={32} className="text-gray-700" />
<p className="text-sm">Schema will appear after database connects</p>
<button
onClick={() => void load()}
className="text-xs text-violet-400 hover:text-violet-300 transition-colors"
>
Retry
</button>
</div>
)
}
const { tables, relationships } = schemaGraph
const positions = layoutTables(tables)
const allX = Object.values(positions).map((p) => p.x + p.w)
const allY = Object.values(positions).map((p) => p.y + p.h)
const svgW = Math.max(...allX) + 40
const svgH = Math.max(...allY) + 40
return (
<div className="h-full overflow-auto p-4">
<div className="text-[10px] text-gray-500 uppercase tracking-widest mb-3 flex items-center gap-1.5">
<GitFork size={10} className="text-violet-400" />
Entity Relationship Diagram
<span className="text-gray-700">Β· {tables.length} tables</span>
</div>
<svg
ref={svgRef}
width={svgW}
height={svgH}
style={{ minWidth: svgW }}
>
{/* FK lines */}
{(relationships as SchemaRelationship[]).map((rel, i) => (
<RelationshipLine
key={i}
from={rel.from}
to={rel.to}
positions={positions}
/>
))}
{/* Tables */}
{tables.map((t: SchemaTable) => (
<TableCard
key={t.name}
table={t}
x={positions[t.name]?.x ?? 0}
y={positions[t.name]?.y ?? 0}
/>
))}
</svg>
</div>
)
}