From 5f1b96ccafb9a7a48ef0ff569557dd17f722a38e Mon Sep 17 00:00:00 2001 From: Chaim Date: Mon, 8 Jun 2026 04:56:01 +0000 Subject: [PATCH] =?UTF-8?q?feat(graph):=20navigation=20&=20UX=20=E2=80=94?= =?UTF-8?q?=20deep-link,=20depth,=20PNG,=20rich=20panel=20(PR=20D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final corpus-graph PR. Connects the graph to the chair's workflow and rounds out the Obsidian-grade interactions. Backend (web/graph_api.py): neighborhood depth cap 2 → 3 (still bounded by NODE_CAP_MAX). Frontend: - URL deep-link: /graph?focus=cl: is read on mount and written on focus change (router.replace, scroll:false). GraphView wrapped in per Next 16's useSearchParams requirement. - "הצג בגרף" button on the precedent detail page → /graph?focus=cl:. - Depth slider (1–3) in the focused overlay → useNodeNeighborhood(id, depth). - Export PNG: grabs the rendered from the area ref → toDataURL → download; failures surface a toast (UI4). - Rich node panel: precedent nodes fetch headnote/summary via the existing usePrecedent hook (Skeleton while pending, error surfaced — UI4). - Edge-type legend (ציטוט / נושא-תחום / יומון) added under the node legend. Deferred (noted for a later pass): expand-in-place merge, search→camera-center. web-ui build + lint pass. Invariants: G2 (depth change is read-only), UI4 (PNG + detail errors surfaced, not swallowed). api:types post-deploy. Co-Authored-By: Claude Opus 4.8 (1M context) --- web-ui/src/app/graph/page.tsx | 11 +- web-ui/src/app/precedents/[id]/page.tsx | 15 ++- .../src/components/graph/graph-node-panel.tsx | 24 ++++ web-ui/src/components/graph/graph-view.tsx | 123 +++++++++++++++--- web/graph_api.py | 2 +- 5 files changed, 152 insertions(+), 23 deletions(-) diff --git a/web-ui/src/app/graph/page.tsx b/web-ui/src/app/graph/page.tsx index c214e02..8e63499 100644 --- a/web-ui/src/app/graph/page.tsx +++ b/web-ui/src/app/graph/page.tsx @@ -1,3 +1,4 @@ +import { Suspense } from "react"; import Link from "next/link"; import { AppShell } from "@/components/app-shell"; @@ -22,7 +23,15 @@ export default function GraphPage() {

- + + טוען גרף… +
+ } + > + +
); diff --git a/web-ui/src/app/precedents/[id]/page.tsx b/web-ui/src/app/precedents/[id]/page.tsx index 55f1b51..a5110fb 100644 --- a/web-ui/src/app/precedents/[id]/page.tsx +++ b/web-ui/src/app/precedents/[id]/page.tsx @@ -2,7 +2,7 @@ import { use, useState } from "react"; import Link from "next/link"; -import { Pencil, Check, X } from "lucide-react"; +import { Pencil, Check, X, Share2 } from "lucide-react"; import { toast } from "sonner"; import { AppShell } from "@/components/app-shell"; import { Card, CardContent } from "@/components/ui/card"; @@ -88,9 +88,16 @@ export default function PrecedentDetailPage({ {data.case_number} - +
+ + +
{/* Citation per Israeli unified citation rules. The LLM diff --git a/web-ui/src/components/graph/graph-node-panel.tsx b/web-ui/src/components/graph/graph-node-panel.tsx index 9aa786b..4efe982 100644 --- a/web-ui/src/components/graph/graph-node-panel.tsx +++ b/web-ui/src/components/graph/graph-node-panel.tsx @@ -12,7 +12,9 @@ import { ExternalLink, X } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; import type { GraphNode } from "@/lib/api/graph"; +import { usePrecedent } from "@/lib/api/precedent-library"; const TYPE_LABELS: Record = { precedent: "פסיקה", @@ -54,6 +56,9 @@ export function GraphNodePanel({ const isPrecedentLike = node.type === "precedent" || node.type === "halacha"; const isGap = node.type === "gap"; const isDigest = node.type === "digest"; + // Rich detail (summary/headnote) for precedents — reuses the library hook. + const detailId = node.type === "precedent" ? node.case_law_id : null; + const detail = usePrecedent(detailId); return ( @@ -119,6 +124,25 @@ export function GraphNodePanel({ )} + {detailId && ( +
+ {detail.isPending ? ( + <> + + + + ) : detail.error ? ( +

לא ניתן לטעון תקציר.

+ ) : detail.data?.headnote || detail.data?.summary ? ( +

+ {detail.data.headnote || detail.data.summary} +

+ ) : ( +

אין תקציר.

+ )} +
+ )} + {isPrecedentLike && node.case_law_id && ( +
-
+
{error ? (
@@ -227,14 +284,30 @@ export function GraphView() { )} {isFocused && ( - +
+ +
+ עומק + setDepth(Number(e.target.value))} + className="w-16 accent-gold" + aria-label="עומק שכנים" + /> + {depth} +
+
)} @@ -352,6 +425,12 @@ const LEGENDS: Record = { ], }; +const EDGE_LEGEND: { color: string; label: string }[] = [ + { color: "rgba(80,90,110,0.7)", label: "ציטוט" }, + { color: "rgba(169,125,58,0.5)", label: "נושא/תחום" }, + { color: "rgba(47,111,122,0.7)", label: "יומון מסכם" }, +]; + function Legend({ colorBy }: { colorBy: ColorBy }) { const items = LEGENDS[colorBy] ?? LEGENDS.type; return ( @@ -365,6 +444,16 @@ function Legend({ colorBy }: { colorBy: ColorBy }) { {i.label}
))} +
+ {EDGE_LEGEND.map((e) => ( +
+ + {e.label} +
+ ))}
); } diff --git a/web/graph_api.py b/web/graph_api.py index 5f89b1b..2378942 100644 --- a/web/graph_api.py +++ b/web/graph_api.py @@ -527,7 +527,7 @@ async def build_node_neighborhood( - ``pa:`` — a practice-area hub; same as topic. """ types = normalize_node_types(node_types) - depth = max(1, min(int(depth), 2)) + depth = max(1, min(int(depth), 3)) # BFS is still bounded by NODE_CAP_MAX prefix, _, rest = node_id.partition(":") rest = rest.strip() if prefix not in {"cl", "tag", "pa"} or not rest: -- 2.49.1