From 6f4e0c7738687066cf41e1e9b676b482842ac5f0 Mon Sep 17 00:00:00 2001 From: hallelx2 Date: Fri, 3 Jul 2026 01:11:25 +0100 Subject: [PATCH] feat(dashboard): render document structure as nested-card tree Replace the flat section list on the document detail page with a recursive, collapsible nested-card tree built from each section's parent_id/depth (same data the graph view uses). Top two levels open by default; deeper levels start collapsed. Documents with no hierarchy degrade gracefully to a flat list of the same landing-styled cards. --- .../dashboard/documents/[docId]/page.tsx | 194 ++++++++++++++---- 1 file changed, 150 insertions(+), 44 deletions(-) diff --git a/apps/web/app/(dashboard)/dashboard/documents/[docId]/page.tsx b/apps/web/app/(dashboard)/dashboard/documents/[docId]/page.tsx index 49b3bf2..c783cc8 100644 --- a/apps/web/app/(dashboard)/dashboard/documents/[docId]/page.tsx +++ b/apps/web/app/(dashboard)/dashboard/documents/[docId]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback, use } from "react"; +import { useState, useCallback, use, useMemo } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import useSWR from "swr"; @@ -24,12 +24,13 @@ import { FileType, GitBranch, Loader2, - ExternalLink, Network, FileText, Download, Copy, Check, + ChevronRight, + ChevronDown, } from "lucide-react"; import { MarkdownSummary } from "@/components/ui/markdown"; import { @@ -58,6 +59,8 @@ interface DocumentDetail { interface TocSection { section_id: string; order: number; + depth?: number; + parent_id?: string; title: string; summary: string; page_range: string; @@ -72,6 +75,148 @@ interface TocData { sections: TocSection[]; } +// ── Nested-card document tree ──────────────────────────────────────── +// Builds a parent→children index from the flat section list (each section +// carries parent_id + depth) and renders it as collapsible nested cards. +// Documents with no hierarchy degrade gracefully to a flat list of cards. + +function buildSectionTree(sections: TocSection[]) { + const byId = new Map(sections.map((s) => [s.section_id, s])); + const childrenOf = new Map(); + const roots: TocSection[] = []; + for (const s of sections) { + const pid = s.parent_id ?? ""; + if (pid && pid !== s.section_id && byId.has(pid)) { + const arr = childrenOf.get(pid) ?? []; + arr.push(s); + childrenOf.set(pid, arr); + } else { + roots.push(s); + } + } + const byOrder = (a: TocSection, b: TocSection) => + (a.order ?? 0) - (b.order ?? 0); + roots.sort(byOrder); + childrenOf.forEach((arr) => arr.sort(byOrder)); + return { childrenOf, roots }; +} + +function SectionTree({ + sections, + docId, +}: { + sections: TocSection[]; + docId: string; +}) { + const { childrenOf, roots } = useMemo( + () => buildSectionTree(sections), + [sections], + ); + return ( +
+ {roots.map((s) => ( + + ))} +
+ ); +} + +function SectionTreeNode({ + section, + childrenOf, + docId, + depth, +}: { + section: TocSection; + childrenOf: Map; + docId: string; + depth: number; +}) { + const kids = childrenOf.get(section.section_id) ?? []; + const hasKids = kids.length > 0; + // Top two levels open by default; deeper levels start collapsed. + const [open, setOpen] = useState(depth < 2); + + return ( +
+
+
+ {hasKids ? ( + + ) : ( + + )} + + + {section.order ?? "•"} + + +
+
+ + {section.title} + + {hasKids && ( + + {kids.length} sub + + )} +
+ {section.summary && ( +
+ +
+ )} +
+ + {section.page_range && ( + + pp. {section.page_range} + + )} +
+
+ + {hasKids && open && ( +
+ {kids.map((k) => ( + + ))} +
+ )} +
+ ); +} + // Engine status enum: pending | parsing | summarizing | ready | failed. // Anything that's not ready or failed counts as "in flight" — keep // polling until it settles. @@ -343,48 +488,9 @@ export default function DocumentDetailPage({ )} )} - {sections.map((section, index) => ( - -
-
-
- - {section.order ?? index + 1} - -

- {section.title} -

-
- {section.summary && ( -
- -
- )} -
-
- {section.page_range && ( - - pp. {section.page_range} - - )} - - - {section.section_id} - -
-
- - ))} + {sections.length > 0 && ( + + )}