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
: {edges.map((edge) => - {edge.kind}{edge.provenance}
)}
};
}
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(" · ") || "—"}
{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 && }
;
}