Expose knowledge graph data in the Memory view through a localized, resilient dashboard panel. - Add protected knowledge graph API routes and access helpers. - Add a Memory view tab, graph panel, loading/error states, and localized labels. - Document graph navigation and cover access, route, hook, and component behavior. Files changed: .changeset/fn-9038-knowledge-graph-tab.md | 7 + docs/dashboard-guide.md | 2 + docs/knowledge-graph.md | 10 ++ packages/dashboard/app/api/legacy.ts | 17 ++- packages/dashboard/app/api/system/index.ts | 1 + .../dashboard/app/api/system/knowledge-graph.ts | 23 +++ .../app/components/KnowledgeGraphPanel.css | 35 +++++ .../app/components/KnowledgeGraphPanel.tsx | 68 +++++++++ packages/dashboard/app/components/MemoryView.tsx | 15 +- .../__tests__/KnowledgeGraphPanel.test.tsx | 49 +++++++ .../app/components/__tests__/MemoryView.test.tsx | 13 ++ .../app/hooks/__tests__/useKnowledgeGraph.test.ts | 66 +++++++++ packages/dashboard/app/hooks/useKnowledgeGraph.ts | 94 ++++++++++++ .../src/__tests__/knowledge-graph-access.test.ts | 88 ++++++++++++ packages/dashboard/src/knowledge-graph-access.ts | 160 +++++++++++++++++++++ .../register-knowledge-graph-routes.test.ts | 75 ++++++++++ .../src/routes/register-knowledge-routes.ts | 120 ++++++++++++++++ packages/i18n/locales/en/app.json | 15 +- packages/i18n/locales/es/app.json | 15 +- packages/i18n/locales/fr/app.json | 15 +- packages/i18n/locales/ko/app.json | 15 +- packages/i18n/locales/pt-BR/app.json | 15 +- packages/i18n/locales/zh-CN/app.json | 15 +- packages/i18n/locales/zh-TW/app.json | 15 +- 24 files changed, 939 insertions(+), 9 deletions(-) Fusion-Task-Id: FN-9038 Fusion-Task-Lineage: 4200e214-ad22-4f92-b980-c9bcb8ffd3b9 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
95 lines
5.3 KiB
TypeScript
95 lines
5.3 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import {
|
|
buildKnowledgeGraphArtifacts,
|
|
fetchKnowledgeGraphNeighbors,
|
|
fetchKnowledgeGraphNode,
|
|
fetchKnowledgeGraphPath,
|
|
fetchKnowledgeGraphStatus,
|
|
queryKnowledgeGraphNodes,
|
|
type KnowledgeGraphNodeQuery,
|
|
type KnowledgeGraphPathResult,
|
|
} from "../api/system/knowledge-graph.js";
|
|
|
|
const EMPTY_FILTERS: KnowledgeGraphNodeQuery = { limit: 50, offset: 0 };
|
|
type NeighborOptions = { direction: "out" | "in" | "both"; edgeKinds?: string[]; depth: number };
|
|
|
|
/*
|
|
FNXC:KnowledgeGraphDashboard 2026-08-13-23:30:
|
|
Graph state is isolated and enabled only by the active tab, so Memory's existing tabs never parse or request the multi-megabyte graph artifacts. Request sequence fences retain the latest operator intent when debounced searches, node drills, or path requests resolve out of order.
|
|
*/
|
|
export function useKnowledgeGraph({ projectId, enabled }: { projectId?: string; enabled: boolean }) {
|
|
const [status, setStatus] = useState<Awaited<ReturnType<typeof fetchKnowledgeGraphStatus>> | null>(null);
|
|
const [filters, setFilters] = useState<KnowledgeGraphNodeQuery>(EMPTY_FILTERS);
|
|
const [nodes, setNodes] = useState<Awaited<ReturnType<typeof queryKnowledgeGraphNodes>> | null>(null);
|
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
const [detail, setDetail] = useState<Awaited<ReturnType<typeof fetchKnowledgeGraphNode>> | null>(null);
|
|
const [neighborOptions, setNeighborOptions] = useState<NeighborOptions>({ direction: "both", depth: 1 });
|
|
const [neighborResults, setNeighborResults] = useState<Awaited<ReturnType<typeof fetchKnowledgeGraphNeighbors>> | null>(null);
|
|
const [pathInput, setPathInput] = useState({ fromId: "", toId: "", maxHops: 6 });
|
|
const [pathResult, setPathResult] = useState<KnowledgeGraphPathResult | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [rebuilding, setRebuilding] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const searchSequence = useRef(0);
|
|
const detailSequence = useRef(0);
|
|
const pathSequence = useRef(0);
|
|
|
|
const refreshStatus = useCallback(async () => {
|
|
try {
|
|
const value = await fetchKnowledgeGraphStatus(projectId);
|
|
setStatus(value);
|
|
setPathInput((input) => ({ ...input, maxHops: value.pathLimits.defaultMaxHops }));
|
|
} catch (reason) { setError(reason instanceof Error ? reason.message : String(reason)); }
|
|
}, [projectId]);
|
|
|
|
useEffect(() => { if (enabled) void refreshStatus(); }, [enabled, refreshStatus]);
|
|
useEffect(() => {
|
|
if (!enabled || !status?.available) return;
|
|
const current = ++searchSequence.current;
|
|
setLoading(true);
|
|
const timer = window.setTimeout(() => void queryKnowledgeGraphNodes(projectId, filters)
|
|
.then((value) => { if (current === searchSequence.current) setNodes(value); })
|
|
.catch((reason) => { if (current === searchSequence.current) setError(reason instanceof Error ? reason.message : String(reason)); })
|
|
.finally(() => { if (current === searchSequence.current) setLoading(false); }), 250);
|
|
return () => window.clearTimeout(timer);
|
|
}, [enabled, filters, projectId, status?.available]);
|
|
|
|
const refreshNeighbors = useCallback(async (id = selectedId) => {
|
|
if (!id) return;
|
|
const current = ++detailSequence.current;
|
|
try {
|
|
const value = await fetchKnowledgeGraphNeighbors(projectId, { nodeId: id, ...neighborOptions });
|
|
if (current === detailSequence.current) setNeighborResults(value);
|
|
} catch (reason) { if (current === detailSequence.current) setError(reason instanceof Error ? reason.message : String(reason)); }
|
|
}, [neighborOptions, projectId, selectedId]);
|
|
|
|
const selectNode = useCallback(async (id: string) => {
|
|
const current = ++detailSequence.current;
|
|
setSelectedId(id);
|
|
try {
|
|
const [node, neighborValue] = await Promise.all([
|
|
fetchKnowledgeGraphNode(projectId, id),
|
|
fetchKnowledgeGraphNeighbors(projectId, { nodeId: id, ...neighborOptions }),
|
|
]);
|
|
if (current === detailSequence.current) { setDetail(node); setNeighborResults(neighborValue); }
|
|
} catch (reason) { if (current === detailSequence.current) setError(reason instanceof Error ? reason.message : String(reason)); }
|
|
}, [neighborOptions, projectId]);
|
|
|
|
const findPath = useCallback(async () => {
|
|
const current = ++pathSequence.current;
|
|
setPathResult(null); setError(null);
|
|
try {
|
|
const result = await fetchKnowledgeGraphPath(projectId, pathInput);
|
|
if (current === pathSequence.current) setPathResult(result);
|
|
} catch (reason) { if (current === pathSequence.current) setError(reason instanceof Error ? reason.message : String(reason)); }
|
|
}, [pathInput, projectId]);
|
|
const rebuild = useCallback(async (force = false) => {
|
|
setRebuilding(true);
|
|
try { await buildKnowledgeGraphArtifacts(projectId, force); await refreshStatus(); }
|
|
catch (reason) { setError(reason instanceof Error ? reason.message : String(reason)); }
|
|
finally { setRebuilding(false); }
|
|
}, [projectId, refreshStatus]);
|
|
|
|
return { status, filters, setFilters, nodes, loading, selectedId, detail, selectNode, neighborOptions, setNeighborOptions, neighborResults, refreshNeighbors, pathInput, setPathInput, pathResult, findPath, rebuild, rebuilding, refreshStatus, error, clearFilters: () => setFilters(EMPTY_FILTERS) };
|
|
}
|