leankg 0.16.7

Lightweight Knowledge Graph for AI-Assisted Development
Documentation
import { useEffect } from 'react';
import { useSigma } from '../hooks/useSigma';
import { createSigmaGraph, filterGraphByDepth } from '../lib/graph-adapter';
import type { KGNode, KGEdge } from '../lib/graph-adapter';
import { CodeViewer } from './CodeViewer';
import { ZoomIn, ZoomOut, Maximize } from 'lucide-react';
import type { ZoomLevel } from '../hooks/useGraphFilters';

interface GraphViewerProps {
  data: { nodes: KGNode[]; relationships: KGEdge[] } | null;
  loading: boolean;
  error: string | null;
  searchTerm?: string;
  visibleEdgeTypes: string[];
  depthFilter: number | null;
  visibleLabels: string[];
  zoomLevel?: ZoomLevel;
  onNodeClick?: (nodeId: string) => void;
  onNodeDoubleClick?: (nodeId: string) => void;
}

export const GraphViewer = ({
  data,
  loading,
  error,
  searchTerm,
  visibleEdgeTypes,
  depthFilter,
  visibleLabels,
  zoomLevel = 'functions',
  onNodeClick,
  onNodeDoubleClick,
}: GraphViewerProps) => {
  const {
    containerRef,
    setGraph,
    zoomIn,
    zoomOut,
    resetZoom,
    selectedNode,
    setSelectedNode,
    sigmaRef,
  } = useSigma({
    visibleEdgeTypes,
    searchTerm,
    onNodeClick,
    onNodeDoubleClick,
  });

  useEffect(() => {
    if (!data || data.nodes.length === 0) return;
    const graph = createSigmaGraph(data.nodes, data.relationships);
    setGraph(graph, true); // Always skip animation - positions are pre-calculated
  }, [data, setGraph]);

  useEffect(() => {
    if (sigmaRef.current && data) {
      const g = sigmaRef.current.getGraph();
      const labels =
        visibleLabels.length > 0
          ? visibleLabels
          : Array.from(new Set(data.nodes.map((n) => String((n.properties as Record<string, unknown>)?.elementType || n.label || ''))));
      filterGraphByDepth(g as unknown as Parameters<typeof filterGraphByDepth>[0], selectedNode, depthFilter, labels);
      sigmaRef.current.refresh();
    }
  }, [depthFilter, selectedNode, visibleLabels, sigmaRef, data, zoomLevel]);

  return (
    <div className="relative w-full h-full bg-[#0A0F24] overflow-hidden flex">
      <div ref={containerRef} className="absolute inset-0 outline-none" />

      {loading && (
        <div className="absolute inset-0 flex flex-col items-center justify-center text-slate-400 bg-[#0A0F24] z-20">
          <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-slate-400 mb-4"></div>
          <p>Analyzing Knowledge Graph...</p>
        </div>
      )}

      {error && (
        <div className="absolute inset-0 flex flex-col items-center justify-center text-red-400 bg-[#0A0F24] z-20">
          <p className="text-xl mb-2">Failed to load graph</p>
          <p className="text-sm opacity-80">{error}</p>
        </div>
      )}

      <div className="absolute top-4 left-4 z-10">
        <div className="bg-[#12182b]/95 backdrop-blur-md border border-slate-700/50 rounded-lg shadow-xl flex flex-col p-1 gap-1">
          <button
            onClick={zoomIn}
            className="p-2 text-slate-400 hover:text-cyan-400 hover:bg-slate-800 rounded-md transition-colors"
            title="Zoom In"
          >
            <ZoomIn className="h-4 w-4" />
          </button>
          <div className="h-px w-full bg-slate-700/50" />
          <button
            onClick={zoomOut}
            className="p-2 text-slate-400 hover:text-cyan-400 hover:bg-slate-800 rounded-md transition-colors"
            title="Zoom Out"
          >
            <ZoomOut className="h-4 w-4" />
          </button>
          <div className="h-px w-full bg-slate-700/50" />
          <button
            onClick={resetZoom}
            className="p-2 text-slate-400 hover:text-cyan-400 hover:bg-slate-800 rounded-md transition-colors"
            title="Fit to screen"
          >
            <Maximize className="h-4 w-4" />
          </button>
        </div>
      </div>

      {selectedNode && (() => {
        const node = data?.nodes.find(n => n.id === selectedNode);
        const elementType = (node?.properties?.elementType as string)?.toLowerCase() || '';
        if (elementType === 'service' || elementType === 'folder') {
          return null;
        }
        return (
          <CodeViewer
            selectedNode={selectedNode}
            graphData={data}
            onClose={() => setSelectedNode(null)}
          />
        );
      })()}
    </div>
  );
};