leankg 0.14.2

Lightweight Knowledge Graph for AI-Assisted Development
Documentation
import { useEffect, useState } from 'react';
import { GraphViewer } from './components/GraphViewer';
import { Database, Search, Network, Target } from 'lucide-react';
import { useGraphFilters } from './hooks/useGraphFilters';
import { EDGE_STYLES, FILTERABLE_LABELS, DEFAULT_VISIBLE_LABELS, NODE_COLORS } from './lib/constants';

function App() {
  const [data, setData] = useState<any>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  // Lifted Graph state
  const { 
    visibleEdgeTypes, 
    toggleEdgeVisibility, 
    depthFilter, 
    setDepthFilter,
    visibleLabels, 
    toggleLabelVisibility,
    setVisibleLabels 
  } = useGraphFilters();
  const [searchTerm, setSearchTerm] = useState('');

  // Initialize display labels properly matching GitNexus defaults
  useEffect(() => {
    setVisibleLabels(DEFAULT_VISIBLE_LABELS);
  }, [setVisibleLabels]);

  useEffect(() => {
    fetch('/api/graph/data')
      .then(res => res.json())
      .then(res => {
        if (res.success && res.data) {
          setData(res.data);
        } else {
          setError(res.error || 'Failed to load graph data');
        }
      })
      .catch(err => setError(err.toString()))
      .finally(() => setLoading(false));
  }, []);

  return (
    <div className="flex h-screen w-full bg-[var(--color-background)] text-[var(--color-text)] overflow-hidden">
      {/* Sidebar */}
      <aside className="w-64 flex-shrink-0 border-r border-slate-800 bg-[#0A0F24] p-6 flex flex-col gap-6 relative z-10 shadow-[rgba(0,0,0,0.5)_4px_0_24px_-4px] overflow-y-auto">
        <div className="flex items-center gap-3">
          <div className="w-10 h-10 rounded-lg bg-blue-600 flex items-center justify-center shadow-[0_0_20px_rgba(37,99,235,0.4)]">
            <Database className="w-6 h-6 text-white" />
          </div>
          <h1 className="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-amber-500">
            LeanKG
          </h1>
        </div>

        <nav className="flex flex-col gap-2 mt-4">
          <button className="w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors duration-200 bg-blue-600/10 text-blue-400">
            <Database className="w-5 h-5" />
            <span className="font-medium">Explorer</span>
          </button>
        </nav>

        <div className="flex flex-col gap-4 mt-2">
          <div className="relative">
            <Search className="absolute left-2.5 top-2 h-4 w-4 text-slate-400" />
            <input 
              type="text" 
              placeholder="Search node..." 
              value={searchTerm}
              onChange={(e) => setSearchTerm(e.target.value)}
              className="w-full bg-slate-900/50 border border-slate-700 text-slate-200 text-sm rounded-lg pl-9 pr-3 py-1.5 focus:outline-none focus:border-cyan-500 transition-colors"
            />
          </div>
          
          <div className="p-4 rounded-xl bg-slate-800/50 border border-slate-700/50 max-h-[300px] overflow-y-auto scrollbar-thin">
            <div className="flex items-center gap-2 mb-3 text-slate-300 font-medium text-xs uppercase tracking-wider">
              <Database className="h-4 w-4 text-slate-400" />
              Node Types
            </div>
            <div className="flex flex-col gap-2">
              {FILTERABLE_LABELS.map((type) => {
                const isActive = visibleLabels.includes(type);
                const color = NODE_COLORS[type] || '#666';
                return (
                  <button
                    key={type}
                    onClick={() => toggleLabelVisibility(type)}
                    className={`w-full px-2 py-1.5 flex items-center gap-3 rounded-md border text-xs transition-colors ${
                      isActive 
                        ? 'bg-slate-800 border-slate-600 text-slate-200' 
                        : 'bg-transparent border-slate-800/80 text-slate-500'
                    }`}
                  >
                    <div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: color }}></div>
                    {type}
                  </button>
                );
              })}
            </div>
          </div>

          <div className="p-4 rounded-xl bg-slate-800/50 border border-slate-700/50">
            <div className="flex items-center gap-2 mb-3 text-slate-300 font-medium text-xs uppercase tracking-wider">
              <Network className="h-4 w-4 text-slate-400" />
              Edge Types
            </div>
            <div className="flex flex-col gap-2">
              {Object.entries(EDGE_STYLES).map(([type, style]) => {
                const isActive = visibleEdgeTypes.length === 0 || visibleEdgeTypes.includes(type);
                return (
                  <button
                    key={type}
                    onClick={() => toggleEdgeVisibility(type)}
                    className={`w-full px-2 py-1.5 flex items-center gap-3 rounded-md border text-xs transition-colors ${
                      isActive 
                        ? 'bg-slate-800 border-slate-600 text-slate-200' 
                        : 'bg-transparent border-slate-800/80 text-slate-500'
                    }`}
                  >
                    <div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: style.color }}></div>
                    {type}
                  </button>
                );
              })}
            </div>
          </div>

          {/* Depth Filter matching GitNexus */}
          <div className="p-4 rounded-xl bg-slate-800/50 border border-slate-700/50">
            <div className="flex items-center gap-2 mb-2 text-slate-300 font-medium text-xs uppercase tracking-wider">
              <Target className="h-4 w-4 text-slate-400" />
              Focus Depth
            </div>
            <p className="mb-3 text-[11px] text-slate-500">
              Show nodes within N hops of selection
            </p>
            <div className="flex flex-wrap gap-1.5">
              {[
                { value: null, label: 'All' },
                { value: 1, label: '1 hop' },
                { value: 2, label: '2 hops' },
                { value: 3, label: '3 hops' },
                { value: 5, label: '5 hops' },
              ].map(({ value, label }) => (
                <button
                  key={label}
                  onClick={() => setDepthFilter(value)}
                  className={`rounded px-2 py-1 border text-xs transition-colors ${
                    depthFilter === value
                      ? 'bg-blue-600 border-blue-500 text-white'
                      : 'bg-transparent border-slate-700 text-slate-400 hover:bg-slate-800 hover:text-slate-200'
                  }`}
                >
                  {label}
                </button>
              ))}
            </div>
          </div>
        </div>

        <div className="mt-auto pt-4">
          <div className="p-4 rounded-xl bg-slate-800/50 border border-slate-700/50">
            <h4 className="text-xs font-mono uppercase tracking-wider text-slate-500 mb-2">Graph Stats</h4>
            {data ? (
              <div className="flex flex-col gap-1">
                <div className="flex justify-between font-mono text-sm">
                  <span className="text-slate-400">Nodes</span>
                  <span className="text-blue-400 font-bold">{data.nodes.length}</span>
                </div>
                <div className="flex justify-between font-mono text-sm">
                  <span className="text-slate-400">Relationships</span>
                  <span className="text-amber-400 font-bold">{data.relationships.length}</span>
                </div>
              </div>
            ) : (
              <p className="text-xs text-slate-500">Loading...</p>
            )}
          </div>
        </div>
      </aside>

      {/* Main Content */}
      <main className="flex-1 relative">
        {loading && (
          <div className="absolute inset-0 flex items-center justify-center bg-[var(--color-background)] z-20">
            <div className="text-center">
              <div className="w-12 h-12 border-4 border-slate-800 border-t-amber-500 rounded-full animate-spin mx-auto mb-4"></div>
              <p className="text-slate-400 font-mono">Loading Graph Engine...</p>
            </div>
          </div>
        )}

        {error && (
          <div className="absolute inset-0 flex items-center justify-center bg-[var(--color-background)] z-20">
            <div className="p-6 bg-red-900/20 border border-red-500/50 rounded-xl max-w-lg text-center">
              <h2 className="text-red-400 font-bold mb-2 text-xl">Connection Error</h2>
              <p className="text-slate-300 font-mono text-sm">{error}</p>
            </div>
          </div>
        )}

        {!loading && !error && data && (
          <GraphViewer 
            data={data} 
            loading={loading} 
            error={error} 
            searchTerm={searchTerm}
            visibleEdgeTypes={visibleEdgeTypes}
            depthFilter={depthFilter}
            visibleLabels={visibleLabels}
          />
        )}
      </main>
    </div>
  );
}

export default App;