From fbea139ae4c77fa74fb5325e967d20edcbbd59df Mon Sep 17 00:00:00 2001 From: pitoi Date: Thu, 2 Jul 2026 17:07:48 +0000 Subject: [PATCH] Generated with Hive: Implement legal-domain seed fetch and update LegalGraphPane mount logic --- .../__tests__/legal-case-files-feed.test.tsx | 90 ++++++ src/lib/__tests__/legal-graph-pane.test.tsx | 169 ++++++++++++ src/lib/__tests__/legal-seed.test.ts | 259 ++++++++++++++++++ src/lib/graph-api.ts | 100 +++++++ src/skins/legal/legal-case-files-feed.tsx | 30 -- src/skins/legal/legal-graph-pane.tsx | 33 ++- 6 files changed, 650 insertions(+), 31 deletions(-) create mode 100644 src/lib/__tests__/legal-case-files-feed.test.tsx create mode 100644 src/lib/__tests__/legal-graph-pane.test.tsx create mode 100644 src/lib/__tests__/legal-seed.test.ts diff --git a/src/lib/__tests__/legal-case-files-feed.test.tsx b/src/lib/__tests__/legal-case-files-feed.test.tsx new file mode 100644 index 0000000..ce7295d --- /dev/null +++ b/src/lib/__tests__/legal-case-files-feed.test.tsx @@ -0,0 +1,90 @@ +/** + * Tests for LegalCaseFilesFeed. + * - Assert getLatestNodes is NEVER called regardless of store state. + */ +import { describe, it, expect, vi, beforeEach } from "vitest" +import { render } from "@testing-library/react" +import React from "react" + +// ── Spy on graph-api to ensure getLatestNodes is never called ───────────────── +const getLatestNodesMock = vi.fn() + +vi.mock("@/lib/graph-api", () => ({ + getLatestNodes: getLatestNodesMock, +})) + +// ── Mock mock-data ──────────────────────────────────────────────────────────── +vi.mock("@/lib/mock-data", () => ({ + isMocksEnabled: () => false, + MOCK_NODES: [], + MOCK_EDGES: [], +})) + +// ── Graph store ─────────────────────────────────────────────────────────────── +const graphState = { + nodes: [] as unknown[], + edges: [], + loading: false, + selectedNode: null, + setSelectedNode: vi.fn(), + setSidebarSelectedNode: vi.fn(), + clearSelection: vi.fn(), + setGraphData: vi.fn(), + setLoading: vi.fn(), +} + +vi.mock("@/stores/graph-store", () => ({ + useGraphStore: (sel?: (s: unknown) => unknown) => { + if (!sel) return graphState + return sel(graphState) + }, +})) + +// ── App store ───────────────────────────────────────────────────────────────── +vi.mock("@/stores/app-store", () => ({ + useAppStore: (sel?: (s: unknown) => unknown) => { + const state = { searchTerm: "" } + return sel ? sel(state) : state + }, +})) + +import { LegalCaseFilesFeed } from "@/skins/legal/legal-case-files-feed" + +beforeEach(() => { + vi.clearAllMocks() + graphState.nodes = [] +}) + +describe("LegalCaseFilesFeed mount", () => { + it("never calls getLatestNodes when store is empty", async () => { + render() + + // Wait for any potential async effects + await new Promise((r) => setTimeout(r, 50)) + + expect(getLatestNodesMock).not.toHaveBeenCalled() + }) + + it("never calls getLatestNodes when store already has nodes", async () => { + graphState.nodes = [{ ref_id: "existing-1", node_type: "Agreement", properties: {} }] + + render() + + await new Promise((r) => setTimeout(r, 50)) + + expect(getLatestNodesMock).not.toHaveBeenCalled() + }) + + it("renders nodes from the store without fetching", async () => { + graphState.nodes = [ + { ref_id: "agr-1", node_type: "Agreement", properties: { title: "Contract Alpha" } }, + ] + + const { getByText } = render() + + await new Promise((r) => setTimeout(r, 50)) + + expect(getByText("Contract Alpha")).toBeDefined() + expect(getLatestNodesMock).not.toHaveBeenCalled() + }) +}) diff --git a/src/lib/__tests__/legal-graph-pane.test.tsx b/src/lib/__tests__/legal-graph-pane.test.tsx new file mode 100644 index 0000000..26d442c --- /dev/null +++ b/src/lib/__tests__/legal-graph-pane.test.tsx @@ -0,0 +1,169 @@ +/** + * Tests for LegalGraphPane mount behavior. + * - Store empty → getLegalInitialNodes called, setGraphData called with result + * - Store non-empty → getLegalInitialNodes NOT called + */ +import { describe, it, expect, vi, beforeEach } from "vitest" +import { render } from "@testing-library/react" +import React from "react" + +// ── Hoist all mocks ─────────────────────────────────────────────────────────── +const { + getLegalInitialNodesMock, + isMocksEnabledMock, + setGraphDataMock, + setLoadingMock, + graphNodes, +} = vi.hoisted(() => { + const graphNodes: unknown[] = [] + return { + getLegalInitialNodesMock: vi.fn(), + isMocksEnabledMock: vi.fn(() => false), + setGraphDataMock: vi.fn(), + setLoadingMock: vi.fn(), + graphNodes, + } +}) + +// ── Module mocks ────────────────────────────────────────────────────────────── +vi.mock("@/lib/graph-api", () => ({ + getLegalInitialNodes: getLegalInitialNodesMock, +})) + +vi.mock("@/lib/mock-data", () => ({ + isMocksEnabled: () => isMocksEnabledMock(), + MOCK_NODES: [{ ref_id: "mock-1", node_type: "Topic", properties: {} }], + MOCK_EDGES: [{ source: "mock-1", target: "mock-2", edge_type: "RELATED" }], +})) + +vi.mock("@/stores/graph-store", () => { + const graphState = { + get nodes() { return graphNodes }, + edges: [], + selectedNode: null, + loadingNeighborRefs: new Set(), + setSelectedNode: vi.fn(), + clearSelection: vi.fn(), + setGraphData: setGraphDataMock, + setLoading: setLoadingMock, + } + const useGraphStore = (sel?: (s: unknown) => unknown) => { + if (!sel) return graphState + return sel(graphState) + } + useGraphStore.getState = () => graphState + return { useGraphStore } +}) + +vi.mock("@/stores/app-store", () => ({ + useAppStore: (sel?: (s: unknown) => unknown) => { + const state = { + graphName: "Test Graph", + searchTerm: "", + sourcesOpen: false, + myContentOpen: false, + followingOpen: false, + agentOpen: false, + clipsOpen: false, + workflowsOpen: false, + closeAllPanels: vi.fn(), + setSearchTerm: vi.fn(), + toggleSources: vi.fn(), + toggleMyContent: vi.fn(), + toggleFollowing: vi.fn(), + toggleAgent: vi.fn(), + toggleWorkflows: vi.fn(), + } + return sel ? sel(state) : state + }, +})) + +vi.mock("@/stores/schema-store", () => ({ + useSchemaStore: (sel?: (s: unknown) => unknown) => { + const state = { schemas: [] } + return sel ? sel(state) : state + }, +})) + +vi.mock("@/components/universe/graph-canvas", () => ({ + GraphCanvas: () =>
, +})) +vi.mock("@/components/search/search-bar", () => ({ + SearchBar: () =>
, +})) +vi.mock("@/components/layout/toolkit", () => ({ + Toolkit: () =>
, + ToolkitFAB: () =>
, +})) + +import { LegalGraphPane } from "@/skins/legal/legal-graph-pane" + +beforeEach(() => { + vi.clearAllMocks() + isMocksEnabledMock.mockReturnValue(false) + // Reset graphNodes to empty + graphNodes.length = 0 +}) + +// ── Tests ───────────────────────────────────────────────────────────────────── + +describe("LegalGraphPane mount", () => { + it("calls getLegalInitialNodes and setGraphData when store is empty", async () => { + const mockResult = { + nodes: [{ ref_id: "agr-1", node_type: "Agreement", properties: {} }], + edges: [], + } + getLegalInitialNodesMock.mockResolvedValue(mockResult) + + render() + + await vi.waitFor(() => { + expect(getLegalInitialNodesMock).toHaveBeenCalledTimes(1) + }) + + expect(setGraphDataMock).toHaveBeenCalledWith(mockResult.nodes, mockResult.edges) + expect(setLoadingMock).toHaveBeenCalledWith(true) + }) + + it("does NOT call getLegalInitialNodes when store already has nodes", async () => { + // Pre-populate graphNodes + graphNodes.push({ ref_id: "existing", node_type: "Agreement", properties: {} }) + + render() + + await new Promise((r) => setTimeout(r, 50)) + + expect(getLegalInitialNodesMock).not.toHaveBeenCalled() + }) + + it("falls back to MOCK_NODES/MOCK_EDGES when mocks are enabled", async () => { + isMocksEnabledMock.mockReturnValue(true) + + render() + + await new Promise((r) => setTimeout(r, 50)) + + expect(getLegalInitialNodesMock).not.toHaveBeenCalled() + expect(setGraphDataMock).toHaveBeenCalledWith( + [{ ref_id: "mock-1", node_type: "Topic", properties: {} }], + [{ source: "mock-1", target: "mock-2", edge_type: "RELATED" }] + ) + }) + + it("logs error and still calls setLoading(false) when getLegalInitialNodes rejects", async () => { + const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + getLegalInitialNodesMock.mockRejectedValue(new Error("network error")) + + render() + + await vi.waitFor(() => { + expect(consoleSpy).toHaveBeenCalledWith( + "[legal-graph-pane] getLegalInitialNodes failed:", + expect.any(Error) + ) + }) + + expect(setLoadingMock).toHaveBeenCalledWith(false) + consoleSpy.mockRestore() + }) +}) diff --git a/src/lib/__tests__/legal-seed.test.ts b/src/lib/__tests__/legal-seed.test.ts new file mode 100644 index 0000000..802e5d9 --- /dev/null +++ b/src/lib/__tests__/legal-seed.test.ts @@ -0,0 +1,259 @@ +/** + * Unit tests for getLegalInitialNodes() and related legal-skin components. + */ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" + +// ── Sphinx / auth mocks ─────────────────────────────────────────────────────── +const { getL402Mock, getSignedMessageMock } = vi.hoisted(() => ({ + getL402Mock: vi.fn(), + getSignedMessageMock: vi.fn(), +})) + +vi.mock("@/lib/sphinx", () => ({ + getL402: getL402Mock, + getSignedMessage: getSignedMessageMock, +})) + +// Real API mode by default (overridden per-test where needed) +vi.mock("@/lib/mock-data", () => ({ + isMocksEnabled: () => false, + MOCK_NODES: [], + MOCK_EDGES: [], + MOCK_REVIEWS: [], + MOCK_WORKFLOW_MARKETPLACE: [], +})) + +import { getLegalInitialNodes } from "@/lib/graph-api" +import type { GraphNode, GraphEdge } from "@/lib/graph-api" + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function makeNode(ref_id: string, node_type: string): GraphNode { + return { ref_id, node_type, properties: { name: ref_id } } +} + +function makeEdge(source: string, target: string, edge_type = "RELATED", ref_id?: string): GraphEdge { + return { source, target, edge_type, ref_id } +} + +// Build a minimal fetch mock that handles all the expected URL patterns +function buildFetchMock(config: { + agreements: GraphNode[] + expansions: Record + topupOrgs?: GraphNode[] + topupPersons?: GraphNode[] +}) { + return vi.fn(async (url: string) => { + const u = url.toString() + + // Agreement latest + if (u.includes("node_type=Agreement")) { + return { ok: true, json: async () => ({ nodes: config.agreements }) } + } + // Organization top-up + if (u.includes("node_type=Organization")) { + return { ok: true, json: async () => ({ nodes: config.topupOrgs ?? [] }) } + } + // Person top-up + if (u.includes("node_type=Person")) { + return { ok: true, json: async () => ({ nodes: config.topupPersons ?? [] }) } + } + // Node expand (/v2/nodes/:refId?expand=edges) + const expandMatch = u.match(/\/v2\/nodes\/([^?]+)\?expand=edges/) + if (expandMatch) { + const refId = expandMatch[1] + const data = config.expansions[refId] ?? { nodes: [], edges: [] } + return { ok: true, json: async () => data } + } + + return { ok: true, json: async () => ({}) } + }) as unknown as typeof fetch +} + +const originalFetch = global.fetch + +beforeEach(() => { + getSignedMessageMock.mockResolvedValue({ signature: "", message: "" }) + getL402Mock.mockResolvedValue(null) +}) + +afterEach(() => { + global.fetch = originalFetch + vi.clearAllMocks() +}) + +// ── getLegalInitialNodes ─────────────────────────────────────────────────────── + +describe("getLegalInitialNodes", () => { + it("returns agreements + expanded orgs/persons and filters edges correctly", async () => { + const agreement = makeNode("agr-1", "Agreement") + const org1 = makeNode("org-1", "Organization") + const person1 = makeNode("per-1", "Person") + const edgeInSet = makeEdge("agr-1", "org-1", "HAS_PARTY", "e-1") + const edgeOutOfSet = makeEdge("agr-1", "unknown-node", "UNRELATED", "e-2") + + global.fetch = buildFetchMock({ + agreements: [agreement], + expansions: { + "agr-1": { + nodes: [org1, person1], + edges: [edgeInSet, edgeOutOfSet], + }, + }, + topupOrgs: [], // not needed — already ≥ 1 (test with < 10 but no top-up orgs) + topupPersons: [], + }) + + const result = await getLegalInitialNodes() + + // Final nodes: 1 agreement + 1 org + 1 person + expect(result.nodes).toHaveLength(3) + const refIds = result.nodes.map((n) => n.ref_id) + expect(refIds).toContain("agr-1") + expect(refIds).toContain("org-1") + expect(refIds).toContain("per-1") + + // Only the edge with both endpoints in the final set is kept + expect(result.edges).toHaveLength(1) + expect(result.edges[0].ref_id).toBe("e-1") + }) + + it("fires top-up for both Org and Person when no neighbors found (0 expansion)", async () => { + const agreement = makeNode("agr-1", "Agreement") + const topupOrg = makeNode("org-tu-1", "Organization") + const topupPerson = makeNode("per-tu-1", "Person") + + global.fetch = buildFetchMock({ + agreements: [agreement], + expansions: { "agr-1": { nodes: [], edges: [] } }, + topupOrgs: [topupOrg], + topupPersons: [topupPerson], + }) + + const result = await getLegalInitialNodes() + + const types = result.nodes.map((n) => n.node_type) + expect(types).toContain("Agreement") + expect(types).toContain("Organization") + expect(types).toContain("Person") + }) + + it("top-up adds exactly 2 more orgs when 8 already found via expansion", async () => { + const agreement = makeNode("agr-1", "Agreement") + // 8 orgs from expansion + const expansionOrgs = Array.from({ length: 8 }, (_, i) => makeNode(`org-${i}`, "Organization")) + const expansionEdges = expansionOrgs.map((o) => + makeEdge("agr-1", o.ref_id, "HAS_PARTY", `e-${o.ref_id}`) + ) + // 5 top-up orgs (only 2 should be taken) + const topupOrgs = Array.from({ length: 5 }, (_, i) => makeNode(`org-tu-${i}`, "Organization")) + + global.fetch = buildFetchMock({ + agreements: [agreement], + expansions: { "agr-1": { nodes: expansionOrgs, edges: expansionEdges } }, + topupOrgs, + topupPersons: [], + }) + + const result = await getLegalInitialNodes() + + const orgNodes = result.nodes.filter((n) => n.node_type === "Organization") + expect(orgNodes).toHaveLength(10) + }) + + it("deduplicates orgs appearing in multiple agreement expansions", async () => { + const agr1 = makeNode("agr-1", "Agreement") + const agr2 = makeNode("agr-2", "Agreement") + const sharedOrg = makeNode("org-shared", "Organization") + const edge1 = makeEdge("agr-1", "org-shared", "HAS_PARTY", "e-1") + const edge2 = makeEdge("agr-2", "org-shared", "HAS_PARTY", "e-2") + + global.fetch = buildFetchMock({ + agreements: [agr1, agr2], + expansions: { + "agr-1": { nodes: [sharedOrg], edges: [edge1] }, + "agr-2": { nodes: [sharedOrg], edges: [edge2] }, + }, + topupOrgs: [], + topupPersons: [], + }) + + const result = await getLegalInitialNodes() + + const orgNodes = result.nodes.filter((n) => n.node_type === "Organization") + expect(orgNodes).toHaveLength(1) + expect(orgNodes[0].ref_id).toBe("org-shared") + }) + + it("excludes edges where one or both endpoints are not in the final node set", async () => { + const agreement = makeNode("agr-1", "Agreement") + const org = makeNode("org-1", "Organization") + const outsider = makeNode("outsider-1", "SomeOtherType") + + const edgeValid = makeEdge("agr-1", "org-1", "HAS_PARTY", "e-valid") + const edgeMissingTarget = makeEdge("agr-1", "outsider-1", "UNRELATED", "e-bad-target") + const edgeMissingBoth = makeEdge("nobody-a", "nobody-b", "UNRELATED", "e-bad-both") + + global.fetch = buildFetchMock({ + agreements: [agreement], + expansions: { + "agr-1": { + nodes: [org, outsider], + edges: [edgeValid, edgeMissingTarget, edgeMissingBoth], + }, + }, + topupOrgs: [], + topupPersons: [], + }) + + const result = await getLegalInitialNodes() + + // outsider is not Agreement/Organization/Person — not in final set + expect(result.edges).toHaveLength(1) + expect(result.edges[0].ref_id).toBe("e-valid") + }) + + it("deduplicates edges by ref_id", async () => { + const agr1 = makeNode("agr-1", "Agreement") + const agr2 = makeNode("agr-2", "Agreement") + const org = makeNode("org-1", "Organization") + + // Same edge ref_id returned from two expansions + const dupEdge1 = makeEdge("agr-1", "org-1", "HAS_PARTY", "dup-edge") + const dupEdge2 = makeEdge("agr-1", "org-1", "HAS_PARTY", "dup-edge") + + global.fetch = buildFetchMock({ + agreements: [agr1, agr2], + expansions: { + "agr-1": { nodes: [org], edges: [dupEdge1] }, + "agr-2": { nodes: [org], edges: [dupEdge2] }, + }, + topupOrgs: [], + topupPersons: [], + }) + + const result = await getLegalInitialNodes() + + expect(result.edges).toHaveLength(1) + }) + + it("deduplicates edges by composite key when ref_id is absent", async () => { + const agreement = makeNode("agr-1", "Agreement") + const org = makeNode("org-1", "Organization") + + // No ref_id on these edges — dedup by source+target+type composite + const edge1: GraphEdge = { source: "agr-1", target: "org-1", edge_type: "HAS_PARTY" } + const edge2: GraphEdge = { source: "agr-1", target: "org-1", edge_type: "HAS_PARTY" } + + global.fetch = buildFetchMock({ + agreements: [agreement], + expansions: { "agr-1": { nodes: [org], edges: [edge1, edge2] } }, + topupOrgs: [], + topupPersons: [], + }) + + const result = await getLegalInitialNodes() + + expect(result.edges).toHaveLength(1) + }) +}) diff --git a/src/lib/graph-api.ts b/src/lib/graph-api.ts index 2217f48..38a29ae 100644 --- a/src/lib/graph-api.ts +++ b/src/lib/graph-api.ts @@ -832,3 +832,103 @@ export async function dismissReview( export async function getSchemaAudit(): Promise { return api.get("/schema/audit") } + +// ------------------------------------------------------------------------- +// Legal Domain Seed +// ------------------------------------------------------------------------- + +// Curated Agreement-first seed for the legal skin. +// 1. Fetch up to 10 Agreement nodes. +// 2. Expand each agreement's 1-hop neighbours; collect Orgs and Persons. +// 3. Deduplicate by ref_id (Maps). +// 4. Top-up to 10 Orgs / 10 Persons if needed. +// 5. Filter edges to only those where both endpoints exist in the final set. +export async function getLegalInitialNodes(signal?: AbortSignal): Promise { + // Step 1: Fetch agreements + const agreementsResp = await api.get( + "/v2/nodes/latest?node_type=Agreement&limit=10&skip_cache=1", + undefined, + signal + ) + const agreements: GraphNode[] = agreementsResp.nodes ?? [] + + // Step 2 & 3: Expand each agreement's neighbours and deduplicate + const orgs = new Map() + const persons = new Map() + const allEdges: GraphEdge[] = [] + + await Promise.all( + agreements.map(async (agreement) => { + try { + const expanded = await getNode(agreement.ref_id, "edges", signal) + for (const node of expanded.nodes ?? []) { + if (node.node_type === "Organization") { + if (!orgs.has(node.ref_id)) orgs.set(node.ref_id, node) + } else if (node.node_type === "Person") { + if (!persons.has(node.ref_id)) persons.set(node.ref_id, node) + } + } + allEdges.push(...(expanded.edges ?? [])) + } catch (err) { + // Skip failed expansions — don't abort the whole seed + console.warn(`[getLegalInitialNodes] expand failed for ${agreement.ref_id}:`, err) + } + }) + ) + + // Step 4: Top-up to 10 of each type + if (orgs.size < 10) { + try { + const topupResp = await api.get( + "/v2/nodes/latest?node_type=Organization&limit=10&skip_cache=1", + undefined, + signal + ) + for (const node of topupResp.nodes ?? []) { + if (orgs.size >= 10) break + if (!orgs.has(node.ref_id)) orgs.set(node.ref_id, node) + } + } catch (err) { + console.warn("[getLegalInitialNodes] Organization top-up failed:", err) + } + } + + if (persons.size < 10) { + try { + const topupResp = await api.get( + "/v2/nodes/latest?node_type=Person&limit=10&skip_cache=1", + undefined, + signal + ) + for (const node of topupResp.nodes ?? []) { + if (persons.size >= 10) break + if (!persons.has(node.ref_id)) persons.set(node.ref_id, node) + } + } catch (err) { + console.warn("[getLegalInitialNodes] Person top-up failed:", err) + } + } + + // Step 5: Build final node set + const finalNodes: GraphNode[] = [ + ...agreements, + ...orgs.values(), + ...persons.values(), + ] + const finalRefIds = new Set(finalNodes.map((n) => n.ref_id)) + + // Step 6: Filter and deduplicate edges + const seenEdges = new Set() + const filteredEdges: GraphEdge[] = [] + for (const edge of allEdges) { + if (!finalRefIds.has(edge.source) || !finalRefIds.has(edge.target)) continue + const key = edge.ref_id + ? edge.ref_id + : `${edge.source}__${edge.target}__${edge.edge_type}` + if (seenEdges.has(key)) continue + seenEdges.add(key) + filteredEdges.push(edge) + } + + return { nodes: finalNodes, edges: filteredEdges } +} diff --git a/src/skins/legal/legal-case-files-feed.tsx b/src/skins/legal/legal-case-files-feed.tsx index 3c69c9e..d9d61f8 100644 --- a/src/skins/legal/legal-case-files-feed.tsx +++ b/src/skins/legal/legal-case-files-feed.tsx @@ -3,9 +3,6 @@ import { useEffect, useMemo, useState } from "react" import { useGraphStore } from "@/stores/graph-store" import { useAppStore } from "@/stores/app-store" -import { useSchemaStore } from "@/stores/schema-store" -import { isMocksEnabled, MOCK_NODES, MOCK_EDGES } from "@/lib/mock-data" -import { getLatestNodes } from "@/lib/graph-api" import { cn } from "@/lib/utils" import type { GraphNode } from "@/lib/graph-api" @@ -88,8 +85,6 @@ export function LegalCaseFilesFeed() { const setSelectedNode = useGraphStore((s) => s.setSelectedNode) const setSidebarSelectedNode = useGraphStore((s) => s.setSidebarSelectedNode) const clearSelection = useGraphStore((s) => s.clearSelection) - const setGraphData = useGraphStore((s) => s.setGraphData) - const setLoading = useGraphStore((s) => s.setLoading) const searchTerm = useAppStore((s) => s.searchTerm) const [activeType, setActiveType] = useState(null) @@ -99,31 +94,6 @@ export function LegalCaseFilesFeed() { setActiveType(null) }, [searchTerm, clearSelection]) - useEffect(() => { - if (useGraphStore.getState().nodes.length > 0) return - if (isMocksEnabled()) { - setGraphData(MOCK_NODES, MOCK_EDGES) - return - } - let cancelled = false - setLoading(true) - ;(async () => { - try { - const result = await getLatestNodes() - if (cancelled) return - setGraphData(result.nodes ?? [], result.edges ?? []) - } catch (err) { - console.error("[legal-feed] getLatestNodes failed:", err) - } finally { - if (!cancelled) setLoading(false) - } - })() - return () => { - cancelled = true - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - const typeCounts = useMemo(() => { const counts = new Map() for (const n of nodes) { diff --git a/src/skins/legal/legal-graph-pane.tsx b/src/skins/legal/legal-graph-pane.tsx index 0e5e773..00dcb58 100644 --- a/src/skins/legal/legal-graph-pane.tsx +++ b/src/skins/legal/legal-graph-pane.tsx @@ -1,5 +1,6 @@ "use client" +import { useEffect } from "react" import { Network, Loader2 } from "lucide-react" import { useGraphStore } from "@/stores/graph-store" import { useAppStore } from "@/stores/app-store" @@ -7,7 +8,8 @@ import { useSchemaStore } from "@/stores/schema-store" import { GraphCanvas } from "@/components/universe/graph-canvas" import { SearchBar } from "@/components/search/search-bar" import { Toolkit, ToolkitFAB } from "@/components/layout/toolkit" -import type { GraphNode } from "@/lib/graph-api" +import { getLegalInitialNodes, type GraphNode } from "@/lib/graph-api" +import { isMocksEnabled, MOCK_NODES, MOCK_EDGES } from "@/lib/mock-data" // ── Legal Network Header ───────────────────────────────────────────────────── @@ -59,6 +61,35 @@ export function LegalGraphPane() { const clearSelection = useGraphStore((s) => s.clearSelection) const loadingNeighbors = useGraphStore((s) => s.loadingNeighborRefs.size > 0) const schemas = useSchemaStore((s) => s.schemas) + const setGraphData = useGraphStore((s) => s.setGraphData) + const setLoading = useGraphStore((s) => s.setLoading) + + useEffect(() => { + if (useGraphStore.getState().nodes.length > 0) return + if (isMocksEnabled()) { + setGraphData(MOCK_NODES, MOCK_EDGES) + return + } + const controller = new AbortController() + let cancelled = false + setLoading(true) + ;(async () => { + try { + const result = await getLegalInitialNodes(controller.signal) + if (cancelled) return + setGraphData(result.nodes, result.edges) + } catch (err) { + console.error("[legal-graph-pane] getLegalInitialNodes failed:", err) + } finally { + if (!cancelled) setLoading(false) + } + })() + return () => { + cancelled = true + controller.abort() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) const sourcesOpen = useAppStore((s) => s.sourcesOpen) const myContentOpen = useAppStore((s) => s.myContentOpen)