Import Solver + neighbors via sparql query

This commit is contained in:
Oxy8
2026-03-04 13:49:14 -03:00
parent d4bfa5f064
commit a75b5b93da
15 changed files with 747 additions and 463 deletions

View File

@@ -5,6 +5,17 @@ function sleep(ms: number): Promise<void> {
return new Promise((r) => setTimeout(r, ms));
}
type GraphMeta = {
backend?: string;
ttl_path?: string | null;
sparql_endpoint?: string | null;
include_bnodes?: boolean;
node_limit?: number;
edge_limit?: number;
nodes?: number;
edges?: number;
};
export default function App() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const rendererRef = useRef<Renderer | null>(null);
@@ -18,12 +29,15 @@ export default function App() {
ptSize: 0,
});
const [error, setError] = useState("");
const [hoveredNode, setHoveredNode] = useState<{ x: number; y: number; screenX: number; screenY: number } | null>(null);
const [hoveredNode, setHoveredNode] = useState<{ x: number; y: number; screenX: number; screenY: number; label?: string; iri?: string } | null>(null);
const [selectedNodes, setSelectedNodes] = useState<Set<number>>(new Set());
const [backendStats, setBackendStats] = useState<{ nodes: number; edges: number; backend?: string } | null>(null);
const graphMetaRef = useRef<GraphMeta | null>(null);
const neighborsReqIdRef = useRef(0);
// Store mouse position in a ref so it can be accessed in render loop without re-renders
const mousePos = useRef({ x: 0, y: 0 });
const nodesRef = useRef<any[]>([]);
useEffect(() => {
const canvas = canvasRef.current;
@@ -70,6 +84,9 @@ export default function App() {
const meta = graph.meta || null;
const count = nodes.length;
nodesRef.current = nodes;
graphMetaRef.current = meta && typeof meta === "object" ? (meta as GraphMeta) : null;
// Build positions from backend-provided node coordinates.
setStatus("Preparing buffers…");
const xs = new Float32Array(count);
@@ -196,9 +213,18 @@ export default function App() {
frameCount++;
// Find hovered node using quadtree
const node = renderer.findNodeAt(mousePos.current.x, mousePos.current.y);
if (node) {
setHoveredNode({ ...node, screenX: mousePos.current.x, screenY: mousePos.current.y });
const hit = renderer.findNodeIndexAt(mousePos.current.x, mousePos.current.y);
if (hit) {
const origIdx = renderer.sortedIndexToOriginalIndex(hit.index);
const meta = origIdx === null ? null : nodesRef.current[origIdx];
setHoveredNode({
x: hit.x,
y: hit.y,
screenX: mousePos.current.x,
screenY: mousePos.current.y,
label: meta && typeof meta.label === "string" ? meta.label : undefined,
iri: meta && typeof meta.iri === "string" ? meta.iri : undefined,
});
} else {
setHoveredNode(null);
}
@@ -234,9 +260,72 @@ export default function App() {
// Sync selection state to renderer
useEffect(() => {
if (rendererRef.current) {
rendererRef.current.updateSelection(selectedNodes);
const renderer = rendererRef.current;
if (!renderer) return;
// Optimistically reflect selection immediately; neighbors will be filled in by backend.
renderer.updateSelection(selectedNodes, new Set());
// Invalidate any in-flight neighbor request for the previous selection.
const reqId = ++neighborsReqIdRef.current;
// Convert selected sorted indices to backend node IDs (graph-export dense IDs).
const selectedIds: number[] = [];
for (const sortedIdx of selectedNodes) {
const origIdx = renderer.sortedIndexToOriginalIndex(sortedIdx);
if (origIdx === null) continue;
const nodeId = nodesRef.current?.[origIdx]?.id;
if (typeof nodeId === "number") selectedIds.push(nodeId);
}
if (selectedIds.length === 0) {
return;
}
// Always send the full current selection list; backend returns the merged neighbor set.
const ctrl = new AbortController();
(async () => {
try {
const meta = graphMetaRef.current;
const body = {
selected_ids: selectedIds,
node_limit: typeof meta?.node_limit === "number" ? meta.node_limit : undefined,
edge_limit: typeof meta?.edge_limit === "number" ? meta.edge_limit : undefined,
};
const res = await fetch("/api/neighbors", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
signal: ctrl.signal,
});
if (!res.ok) throw new Error(`POST /api/neighbors failed: ${res.status}`);
const data = await res.json();
if (ctrl.signal.aborted) return;
if (reqId !== neighborsReqIdRef.current) return;
const neighborIds: unknown = data?.neighbor_ids;
const neighborSorted = new Set<number>();
if (Array.isArray(neighborIds)) {
for (const id of neighborIds) {
if (typeof id !== "number") continue;
const sorted = renderer.vertexIdToSortedIndexOrNull(id);
if (sorted === null) continue;
if (!selectedNodes.has(sorted)) neighborSorted.add(sorted);
}
}
renderer.updateSelection(selectedNodes, neighborSorted);
} catch (e) {
if (ctrl.signal.aborted) return;
console.warn(e);
// Keep the UI usable even if neighbors fail to load.
renderer.updateSelection(selectedNodes, new Set());
}
})();
return () => ctrl.abort();
}, [selectedNodes]);
return (
@@ -350,7 +439,12 @@ export default function App() {
boxShadow: "0 2px 8px rgba(0,0,0,0.5)",
}}
>
({hoveredNode.x.toFixed(2)}, {hoveredNode.y.toFixed(2)})
<div style={{ color: "#0ff" }}>
{hoveredNode.label || hoveredNode.iri || "(unknown)"}
</div>
<div style={{ color: "#688" }}>
({hoveredNode.x.toFixed(2)}, {hoveredNode.y.toFixed(2)})
</div>
</div>
)}
</>