mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
fix(explorer): address grouped view review blockers
- Fix `import.meta.env.DEV` crash in graphSceneState.ts that broke the entire test:graph-workspace suite (module load fails in Node.js/tsx) - Export `resolveGroupedDisplayNodeId` from graphSceneState.ts and remove the identical copy in GraphWorkspace.tsx - Add `checkGroupedViewAvailability` helper (Louvain only, no centrality) so grouped view availability can be checked cheaply on every graph change - Gate full community graph build (`groupedDisplayCandidate`) on `viewMode === 'grouped'` to avoid running Louvain + centrality on every graph version tick when the user is not in grouped view - Remove dead ternary in `focusNode` where both branches returned `nodeId` - Add 7 new tests covering resolveGroupedDisplayNodeId, resolveGroupedDisplayStateSnapshot, and checkGroupedViewAvailability Co-Authored-By: ZohaibHassan16 <109234410+ZohaibHassan16@users.noreply.github.com> Co-Authored-By: KaifAhmad1 <mohammadk78600@gmail.com>
This commit is contained in:
co-authored by
ZohaibHassan16
KaifAhmad1
parent
b010ba68fa
commit
6ad1502224
@@ -12,7 +12,7 @@ import { useLoadGraph, useReloadGraph } from "./useLoadGraph";
|
||||
import { GraphLoadingOverlay } from "./GraphLoadingOverlay";
|
||||
import { createGraphLoadProgress, getGraphLoadTitle } from "./graphLoading";
|
||||
import { GRAPH_THEME, withAlpha } from "./graphTheme";
|
||||
import { resolveDisplayGraph, resolveDisplayStateSnapshot, resolveGroupedDisplayStateSnapshot } from "./graphSceneState";
|
||||
import { checkGroupedViewAvailability, resolveDisplayGraph, resolveDisplayStateSnapshot, resolveGroupedDisplayNodeId, resolveGroupedDisplayStateSnapshot } from "./graphSceneState";
|
||||
import {
|
||||
type GraphPlugin,
|
||||
type GraphPluginActionRequest,
|
||||
@@ -548,47 +548,6 @@ type FocusResolution = {
|
||||
reason: string | null;
|
||||
};
|
||||
|
||||
function resolveGroupedDisplayNodeId(
|
||||
displayGraph: GraphSceneRuntime["displayGraph"],
|
||||
nodeId: string,
|
||||
): string | null {
|
||||
if (!nodeId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (displayGraph.hasNode(nodeId)) {
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
let resolvedNodeId: string | null = null;
|
||||
displayGraph.forEachNode((candidateId, attrs) => {
|
||||
if (resolvedNodeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const communityGroup = (attrs as NodeAttributes).properties?.__communityGroup as
|
||||
| {
|
||||
anchorNodeId?: string | null;
|
||||
memberNodeIds?: string[];
|
||||
sampleNodeIds?: string[];
|
||||
}
|
||||
| undefined;
|
||||
if (!communityGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
communityGroup.anchorNodeId === nodeId
|
||||
|| communityGroup.sampleNodeIds?.includes(nodeId)
|
||||
|| communityGroup.memberNodeIds?.includes(nodeId)
|
||||
) {
|
||||
resolvedNodeId = candidateId;
|
||||
}
|
||||
});
|
||||
|
||||
return resolvedNodeId;
|
||||
}
|
||||
|
||||
function buildSelectedEdgeState(
|
||||
edgeId: string,
|
||||
displayGraph: typeof graph | Graph<NodeAttributes, EdgeAttributes>,
|
||||
@@ -953,15 +912,19 @@ export function GraphWorkspace() {
|
||||
);
|
||||
const inspectableNodeId = focusedSelectionResolution.resolvedNodeId ?? "";
|
||||
const canActivateFocusedMode = Boolean(focusedSelectionResolution.resolvedNodeId);
|
||||
const { available: groupedViewAvailable, reason: groupedViewReason } = useMemo(
|
||||
() => checkGroupedViewAvailability(),
|
||||
[graphVersion],
|
||||
);
|
||||
const groupedDisplayCandidate = useMemo(
|
||||
() => resolveDisplayGraph("", EMPTY_PATH, EMPTY_PATH, "grouped", {
|
||||
aggregationEnabled,
|
||||
collapsedNeighborhoodNodeIds,
|
||||
}),
|
||||
[aggregationEnabled, collapsedNeighborhoodNodeIds, graphVersion],
|
||||
() => viewMode === "grouped"
|
||||
? resolveDisplayGraph("", EMPTY_PATH, EMPTY_PATH, "grouped", {
|
||||
aggregationEnabled,
|
||||
collapsedNeighborhoodNodeIds,
|
||||
})
|
||||
: null,
|
||||
[viewMode, aggregationEnabled, collapsedNeighborhoodNodeIds, graphVersion],
|
||||
);
|
||||
const groupedViewAvailable = groupedDisplayCandidate.state.groupedViewAvailable;
|
||||
const groupedViewReason = groupedDisplayCandidate.state.groupedViewReason;
|
||||
|
||||
const requestViewMode = useCallback((nextViewMode: GraphViewMode) => {
|
||||
if (nextViewMode === "focused") {
|
||||
@@ -986,7 +949,11 @@ export function GraphWorkspace() {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupedDisplayGraph = groupedDisplayCandidate.graph;
|
||||
const groupedDisplayGraph = groupedDisplayCandidate?.graph
|
||||
?? resolveDisplayGraph("", EMPTY_PATH, EMPTY_PATH, "grouped", {
|
||||
aggregationEnabled,
|
||||
collapsedNeighborhoodNodeIds,
|
||||
}).graph;
|
||||
const nextGroupedSelection = [
|
||||
lastGroupedSelectedNodeId,
|
||||
selectedNodeId,
|
||||
@@ -1016,7 +983,7 @@ export function GraphWorkspace() {
|
||||
collapsedNeighborhoodNodeIds,
|
||||
focusedNodeId,
|
||||
graphVersion,
|
||||
groupedDisplayCandidate.graph,
|
||||
groupedDisplayCandidate,
|
||||
groupedViewAvailable,
|
||||
groupedViewReason,
|
||||
lastGroupedSelectedNodeId,
|
||||
@@ -1030,9 +997,7 @@ export function GraphWorkspace() {
|
||||
}
|
||||
|
||||
const currentDisplayGraph = pluginRuntimeRef.current?.displayGraph ?? graph;
|
||||
const nextSelectedNodeId = viewMode === "focused" && graph.hasNode(nodeId)
|
||||
? nodeId
|
||||
: nodeId;
|
||||
const nextSelectedNodeId = nodeId;
|
||||
|
||||
if (!graph.hasNode(nodeId) && currentDisplayGraph.hasNode(nodeId)) {
|
||||
setLastGroupedSelectedNodeId(nodeId);
|
||||
@@ -1218,7 +1183,10 @@ export function GraphWorkspace() {
|
||||
const displayResult = useMemo(
|
||||
() => (
|
||||
viewMode === "grouped"
|
||||
? groupedDisplayCandidate
|
||||
? (groupedDisplayCandidate ?? resolveDisplayGraph("", EMPTY_PATH, EMPTY_PATH, "grouped", {
|
||||
aggregationEnabled,
|
||||
collapsedNeighborhoodNodeIds,
|
||||
}))
|
||||
: resolveDisplayGraph(structuralSelectedNodeId, structuralActivePath, structuralActivePathEdgeIds, viewMode, {
|
||||
aggregationEnabled,
|
||||
collapsedNeighborhoodNodeIds,
|
||||
|
||||
@@ -35,7 +35,7 @@ const COLLAPSE_VISIBLE_NEIGHBORS = 8;
|
||||
const GROUP_SAMPLE_MEMBERS = 8;
|
||||
const AGGREGATED_EDGE_PREFIX = "__agg__:";
|
||||
const COMMUNITY_NODE_PREFIX = "__community__:";
|
||||
const DEBUG_GRAPH_SCENE_STATE = import.meta.env.DEV;
|
||||
const DEBUG_GRAPH_SCENE_STATE = typeof import.meta !== "undefined" && import.meta.env?.DEV === true;
|
||||
|
||||
type GraphRef = typeof graph | Graph<NodeAttributes, EdgeAttributes>;
|
||||
|
||||
@@ -159,7 +159,7 @@ function createEmptyDisplayState(
|
||||
};
|
||||
}
|
||||
|
||||
function resolveGroupedDisplayNodeId(
|
||||
export function resolveGroupedDisplayNodeId(
|
||||
displayGraph: GraphRef,
|
||||
nodeId: string,
|
||||
): string | null {
|
||||
@@ -201,6 +201,18 @@ function resolveGroupedDisplayNodeId(
|
||||
return resolvedNodeId;
|
||||
}
|
||||
|
||||
export function checkGroupedViewAvailability(): { available: boolean; reason: string | null } {
|
||||
const base = computeGraphAnalyticsBase(graph, {
|
||||
computeCommunities: true,
|
||||
computeCentrality: false,
|
||||
});
|
||||
const available = base.communitiesByNode.size > 0;
|
||||
return {
|
||||
available,
|
||||
reason: available ? null : "Grouped view is unavailable until communities can be detected.",
|
||||
};
|
||||
}
|
||||
|
||||
function rankGroupedNeighbors(
|
||||
displayGraph: GraphRef,
|
||||
nodeId: string,
|
||||
|
||||
@@ -6,7 +6,12 @@ import {
|
||||
batchMergeNodes,
|
||||
clearGraph,
|
||||
} from "../src/store/graphStore.ts";
|
||||
import { resolveDisplayGraph } from "../src/workspaces/GraphWorkspace/graphSceneState.ts";
|
||||
import {
|
||||
checkGroupedViewAvailability,
|
||||
resolveDisplayGraph,
|
||||
resolveGroupedDisplayNodeId,
|
||||
resolveGroupedDisplayStateSnapshot,
|
||||
} from "../src/workspaces/GraphWorkspace/graphSceneState.ts";
|
||||
|
||||
function addNode(id: string, semanticGroup = "entity") {
|
||||
batchMergeNodes([
|
||||
@@ -135,3 +140,120 @@ test("resolveDisplayGraph grouped view emits community nodes and edges", () => {
|
||||
|
||||
assert.equal(hasCommunityEdge, true);
|
||||
});
|
||||
|
||||
// ── resolveGroupedDisplayNodeId ──────────────────────────────────────────────
|
||||
|
||||
test("resolveGroupedDisplayNodeId returns null for empty nodeId", () => {
|
||||
const { graph: displayGraph } = resolveDisplayGraph("", [], [], "full", { aggregationEnabled: false });
|
||||
assert.equal(resolveGroupedDisplayNodeId(displayGraph, ""), null);
|
||||
});
|
||||
|
||||
test("resolveGroupedDisplayNodeId returns nodeId when it exists directly in display graph", () => {
|
||||
addNode("x");
|
||||
const { graph: displayGraph } = resolveDisplayGraph("", [], [], "full", { aggregationEnabled: false });
|
||||
assert.equal(resolveGroupedDisplayNodeId(displayGraph, "x"), "x");
|
||||
});
|
||||
|
||||
test("resolveGroupedDisplayNodeId resolves base node to its community node", () => {
|
||||
const left = ["a1", "a2", "a3", "a4"];
|
||||
const right = ["b1", "b2", "b3", "b4"];
|
||||
[...left, ...right].forEach((nodeId, index) => addNode(nodeId, index < left.length ? "left" : "right"));
|
||||
|
||||
let edgeIndex = 0;
|
||||
for (let i = 0; i < left.length; i += 1) {
|
||||
for (let j = 0; j < left.length; j += 1) {
|
||||
if (i !== j) addEdge(`l-${edgeIndex++}`, left[i], left[j], 3);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < right.length; i += 1) {
|
||||
for (let j = 0; j < right.length; j += 1) {
|
||||
if (i !== j) addEdge(`r-${edgeIndex++}`, right[i], right[j], 3);
|
||||
}
|
||||
}
|
||||
addEdge("bridge-1", "a1", "b1", 0.1);
|
||||
|
||||
const { graph: displayGraph } = resolveDisplayGraph("", [], [], "grouped", { aggregationEnabled: true });
|
||||
const communityNodes = displayGraph.nodes().filter((n) => n.startsWith("__community__"));
|
||||
assert.ok(communityNodes.length >= 2, "expected community nodes");
|
||||
|
||||
const resolved = resolveGroupedDisplayNodeId(displayGraph, "a1");
|
||||
assert.ok(resolved !== null, "should resolve a1 to a community node");
|
||||
assert.ok(resolved!.startsWith("__community__"), "resolved id should be a community node");
|
||||
});
|
||||
|
||||
// ── resolveGroupedDisplayStateSnapshot ──────────────────────────────────────
|
||||
|
||||
test("resolveGroupedDisplayStateSnapshot returns none-kind when no node selected", () => {
|
||||
addNode("p");
|
||||
addNode("q");
|
||||
addEdge("e1", "p", "q");
|
||||
const { graph: displayGraph } = resolveDisplayGraph("", [], [], "full", { aggregationEnabled: false });
|
||||
const state = resolveGroupedDisplayStateSnapshot(displayGraph, "", {
|
||||
groupedViewAvailable: true,
|
||||
groupedViewReason: null,
|
||||
});
|
||||
assert.equal(state.selectedNodeKind, "none");
|
||||
assert.equal(state.selectedRootNodeId, null);
|
||||
});
|
||||
|
||||
test("resolveGroupedDisplayStateSnapshot maps selected base node to community in grouped graph", () => {
|
||||
const left = ["c1", "c2", "c3", "c4"];
|
||||
const right = ["d1", "d2", "d3", "d4"];
|
||||
[...left, ...right].forEach((nodeId, index) => addNode(nodeId, index < left.length ? "left" : "right"));
|
||||
|
||||
let edgeIndex = 0;
|
||||
for (let i = 0; i < left.length; i += 1) {
|
||||
for (let j = 0; j < left.length; j += 1) {
|
||||
if (i !== j) addEdge(`lc-${edgeIndex++}`, left[i], left[j], 3);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < right.length; i += 1) {
|
||||
for (let j = 0; j < right.length; j += 1) {
|
||||
if (i !== j) addEdge(`rc-${edgeIndex++}`, right[i], right[j], 3);
|
||||
}
|
||||
}
|
||||
addEdge("bridge-c1", "c1", "d1", 0.1);
|
||||
addEdge("bridge-c2", "c2", "d2", 0.1);
|
||||
|
||||
const { graph: displayGraph } = resolveDisplayGraph("", [], [], "grouped", { aggregationEnabled: true });
|
||||
const state = resolveGroupedDisplayStateSnapshot(displayGraph, "c1", {
|
||||
groupedViewAvailable: true,
|
||||
groupedViewReason: null,
|
||||
selectedNodeKind: "grouped",
|
||||
});
|
||||
|
||||
assert.ok(state.selectedRootNodeId !== null, "should resolve to a community node");
|
||||
assert.ok(state.selectedRootNodeId!.startsWith("__community__"), "root should be a community node");
|
||||
assert.equal(state.groupedViewAvailable, true);
|
||||
});
|
||||
|
||||
// ── checkGroupedViewAvailability ─────────────────────────────────────────────
|
||||
|
||||
test("checkGroupedViewAvailability returns unavailable on empty graph", () => {
|
||||
const result = checkGroupedViewAvailability();
|
||||
assert.equal(result.available, false);
|
||||
assert.ok(typeof result.reason === "string" && result.reason.length > 0);
|
||||
});
|
||||
|
||||
test("checkGroupedViewAvailability returns available when communities exist", () => {
|
||||
const left = ["e1", "e2", "e3", "e4"];
|
||||
const right = ["f1", "f2", "f3", "f4"];
|
||||
[...left, ...right].forEach((nodeId, index) => addNode(nodeId, index < left.length ? "left" : "right"));
|
||||
|
||||
let edgeIndex = 0;
|
||||
for (let i = 0; i < left.length; i += 1) {
|
||||
for (let j = 0; j < left.length; j += 1) {
|
||||
if (i !== j) addEdge(`le-${edgeIndex++}`, left[i], left[j], 3);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < right.length; i += 1) {
|
||||
for (let j = 0; j < right.length; j += 1) {
|
||||
if (i !== j) addEdge(`re-${edgeIndex++}`, right[i], right[j], 3);
|
||||
}
|
||||
}
|
||||
addEdge("bridge-e1", "e1", "f1", 0.1);
|
||||
|
||||
const result = checkGroupedViewAvailability();
|
||||
assert.equal(result.available, true);
|
||||
assert.equal(result.reason, null);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user