import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useKnowledgeGraph } from "../hooks/useKnowledgeGraph.js"; import type { KnowledgeGraphEdge } from "../api/system/knowledge-graph.js"; import "./KnowledgeGraphPanel.css"; const NODE_KINDS = ["file", "module", "symbol", "doc-concept", "rationale"] as const; /* FNXC:KnowledgeGraphDashboard 2026-08-13-23:21: Symbol filter values must exactly match the core SymbolKind union because the API rejects unknown enum values rather than silently widening a query. */ const SYMBOL_KINDS = ["function", "class", "interface", "type-alias", "enum", "variable", "namespace", "alias"] as const; const EDGE_KINDS = ["contains", "imports", "re-exports", "relates-to", "rationale-supports"] as const; /** FNXC:KnowledgeGraphDashboard 2026-08-13-23:30: Each result is a bounded graph slice; node-to-node buttons are the navigation model instead of a whole-graph canvas. */ function EndpointButton({ id, onSelect }: { id: string; onSelect: (id: string) => void }) { return ; } function EdgeList({ title, edges, total, endpoint, onSelect }: { title: string; edges: KnowledgeGraphEdge[]; total: number; endpoint: "from" | "to"; onSelect: (id: string) => void }) { return

{title} showing {edges.length} of {total}

{edges.length === 0 ?

None

: }
; } export function KnowledgeGraphPanel({ projectId, addToast }: { projectId?: string; addToast: (message: string, type: "success" | "error" | "info") => void }) { const { t } = useTranslation("app"); const graph = useKnowledgeGraph({ projectId, enabled: true }); const [force, setForce] = useState(false); useEffect(() => { if (graph.error) addToast(graph.error, "error"); }, [addToast, graph.error]); /* FNXC:KnowledgeGraphDashboard 2026-08-13-23:21: A failed status request is an error state, not an indefinite loading state, so operators can distinguish an unavailable graph artifact from a failed dashboard request. */ if (!graph.status && graph.error) return
{graph.error}
; if (!graph.status) return
{t("memory.graphLoading", "Loading knowledge graph…")}
; if (!graph.status.available) return

{t("memory.graphUnavailable", "Graph unavailable")}: {graph.status.recoveryReason}

; const setFilter = (patch: Partial) => graph.setFilters({ ...graph.filters, ...patch, offset: 0 }); const updateKinds = (kind: string) => { const kinds = graph.filters.kinds ?? []; setFilter({ kinds: kinds.includes(kind) ? kinds.filter((item) => item !== kind) : [...kinds, kind] }); }; const updateEdgeKinds = (kind: string) => { const edgeKinds = graph.neighborOptions.edgeKinds ?? []; graph.setNeighborOptions({ ...graph.neighborOptions, edgeKinds: edgeKinds.includes(kind) ? edgeKinds.filter((item) => item !== kind) : [...edgeKinds, kind] }); }; const selected = graph.selectedId; const limits = graph.status.pathLimits; const foundPath = graph.pathResult?.outcome === "found" ? graph.pathResult.path : null; const foundHops = graph.pathResult?.outcome === "found" ? graph.pathResult.hops : null; return
{graph.status.nodeCount} {t("memory.graphNodes", "nodes")} · {graph.status.edgeCount} {t("memory.graphEdges", "edges")}{graph.status.graphDir}
Node kinds
{Object.entries(graph.status.nodeKindCounts ?? {}).map(([kind, count]) => `${kind}: ${count}`).join(" · ") || "—"}
Edge kinds
{Object.entries(graph.status.edgeKindCounts ?? {}).map(([kind, count]) => `${kind}: ${count}`).join(" · ") || "—"}
Provenance
{Object.entries(graph.status.provenanceCounts ?? {}).map(([kind, count]) => `${kind}: ${count}`).join(" · ") || "—"}
{NODE_KINDS.map((kind) => )}
setFilter({ pathPrefix: event.target.value })} /> setFilter({ idPrefix: event.target.value })} /> setFilter({ namePattern: event.target.value })} />

{graph.nodes?.total ?? 0} {t("memory.graphResults", "results")}

{graph.loading ?

Searching…

: graph.nodes?.nodes.length ? <>
    {graph.nodes.nodes.map((node) =>
  • )}
:

{t("memory.graphNoResults", "No matching nodes.")}

}
{graph.detail ? <>

{graph.detail.node.name}

{graph.detail.node.id}

{graph.detail.node.ownerPath}:{graph.detail.node.source.line}

void graph.selectNode(id)} /> void graph.selectNode(id)} /> :

Select a node to inspect its edges.

}
{graph.detail &&

Neighbors

{EDGE_KINDS.map((kind) => )}
{graph.neighborResults?.neighbors.length ?
    {graph.neighborResults.neighbors.map((neighbor) =>
  • distance {neighbor.distance} void graph.selectNode(id)} />
  • )}
:

No neighbors found.

}
}

Shortest path

graph.setPathInput({ ...graph.pathInput, fromId: event.target.value })} /> graph.setPathInput({ ...graph.pathInput, toId: event.target.value })} /> graph.setPathInput({ ...graph.pathInput, maxHops: Number(event.target.value) })} />{selected && <>}{foundPath &&
{foundHops} hops
{foundPath.nodes.map((node, index) => void graph.selectNode(id)} />{index < foundPath.edges.length && {foundPath.edges[index].kind}})}
}{graph.pathResult?.outcome === "not-found" &&

No path exists between these nodes.

}{graph.pathResult?.outcome === "limit-reached" &&

No path found within {graph.pathResult.maxHops} hops; {graph.pathResult.limit} was reached.

{graph.pathInput.maxHops < limits.maxHops && }
}{graph.error &&

{graph.error}

}
; }