"use client"; /** * Corpus graph orchestrator. Owns filter + selection state, decides whether to * render the full graph or a focused node neighborhood (the Obsidian "local * graph"), and wires the filter sidebar, canvas, and node detail panel. */ import { useEffect, useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { type CorpusGraph, type GraphNode, useCorpusGraph, useGraphFacets, useNodeNeighborhood, } from "@/lib/api/graph"; import { type GraphControls, GraphFilterPanel, } from "@/components/graph/graph-filter-panel"; import { GraphCanvas } from "@/components/graph/graph-canvas"; import { GraphNodePanel } from "@/components/graph/graph-node-panel"; const NODE_LIMIT = 400; function useDebouncedValue(value: T, ms: number): T { const [debounced, setDebounced] = useState(value); useEffect(() => { const id = setTimeout(() => setDebounced(value), ms); return () => clearTimeout(id); }, [value, ms]); return debounced; } export function GraphView() { const [controls, setControls] = useState({ practiceArea: "", source: "", minCitations: 0, q: "", court: "", precedentLevel: "", chair: "", district: "", yearFrom: 0, yearTo: 0, showTopics: true, showPracticeAreas: true, showHalachot: false, }); const facets = useGraphFacets().data; const [selectedNode, setSelectedNode] = useState(null); const [focusNodeId, setFocusNodeId] = useState(null); const onChange = (patch: Partial) => setControls((c) => ({ ...c, ...patch })); const nodeTypes = useMemo(() => { const t = ["precedent"]; if (controls.showTopics) t.push("topic"); if (controls.showPracticeAreas) t.push("practice_area"); if (controls.showHalachot) t.push("halacha"); return t.join(","); }, [controls.showTopics, controls.showPracticeAreas, controls.showHalachot]); const debouncedQ = useDebouncedValue(controls.q, 350); const filters = useMemo( () => ({ practice_area: controls.practiceArea, source: controls.source, min_citations: controls.minCitations, node_types: nodeTypes, limit: NODE_LIMIT, q: debouncedQ, court: controls.court, precedent_level: controls.precedentLevel, chair: controls.chair, district: controls.district, year_from: controls.yearFrom, year_to: controls.yearTo, }), [ controls.practiceArea, controls.source, controls.minCitations, controls.court, controls.precedentLevel, controls.chair, controls.district, controls.yearFrom, controls.yearTo, nodeTypes, debouncedQ, ], ); const isFocused = !!focusNodeId; const full = useCorpusGraph(filters, !isFocused); const neighborhood = useNodeNeighborhood(focusNodeId, 1, nodeTypes); const active = isFocused ? neighborhood : full; const data: CorpusGraph | undefined = active.data; const error = active.error as Error | undefined; const handleNodeClick = (node: GraphNode) => { setSelectedNode(node); setFocusNodeId(node.id); }; const backToFull = () => { setFocusNodeId(null); setSelectedNode(null); }; return (
{data ? `${data.nodes.length} נקודות · ${data.edges.length} קשרים` : "—"} {!isFocused && full.data?.truncated && ( מוצגות {full.data.nodes.length} הנקודות המצוטטות ביותר מתוך{" "} {full.data.total_available} — צמצמו את הסינון כדי לראות פחות )}
{error ? (

שגיאה בטעינת הגרף

{error.message}

) : active.isLoading && !data ? (
טוען גרף…
) : data && data.nodes.length === 0 ? (
אין נקודות התואמות לסינון.
) : ( )} {isFocused && ( )}
{selectedNode && ( setSelectedNode(null)} /> )}
); } function Legend() { const items = [ { color: "#1e3a5f", label: "פסיקה" }, { color: "#a97d3a", label: "נושא" }, { color: "#475569", label: "תחום" }, ]; return (
{items.map((i) => (
{i.label}
))}
); }