disclosure-bureau/web/app/d/[docId]/[page]/page.tsx

180 lines
6.7 KiB
TypeScript
Raw Normal View History

/**
* /d/[docId]/[page] single-page chunks view.
*
* Scoped to one page (e.g., p007). Shows the PNG of the page alongside
* the chunks for cross-reference.
*/
import Link from "next/link";
import Image from "next/image";
import { notFound } from "next/navigation";
import { readChunksByPage, readIndex, hasChunks } from "@/lib/chunks";
import { readDocument, listDocuments } from "@/lib/wiki";
import { AuthBar } from "@/components/auth-bar";
import { ChatBubble } from "@/components/chat-bubble";
import { DocRendererV2 } from "@/components/doc-renderer-v2";
export const dynamic = "force-dynamic";
export default async function DocPageView({
params,
}: {
params: Promise<{ docId: string; page: string }>;
}) {
const { docId, page } = await params;
const stem = /^p\d{3}$/.test(page) ? page : `p${page.padStart(3, "0")}`;
const m = stem.match(/^p(\d{3})$/);
if (!m) notFound();
const pageNum = parseInt(m[1], 10);
if (!(await hasChunks(docId))) {
return (
<main className="min-h-screen p-6 md:p-10 max-w-4xl mx-auto">
<div className="flex items-start justify-between gap-4 mb-6">
<Link href={`/d/${docId}`} className="font-mono text-xs text-[#7fdbff] hover:text-[#00ff9c]">
documento
</Link>
<AuthBar />
</div>
<div className="border border-[rgba(0,255,156,0.15)] bg-[#0a121e] rounded p-6">
<h1 className="font-mono text-lg text-[#00ff9c] mb-2"> documento ainda não indexado</h1>
<p className="text-[#c8d4e6] text-sm">
Este documento ainda não foi processado.
</p>
</div>
</main>
);
}
const [idx, byPage, doc, docList] = await Promise.all([
readIndex(docId),
readChunksByPage(docId),
readDocument(docId),
listDocuments(),
]);
if (!idx) notFound();
const pageChunks = byPage.get(pageNum) ?? [];
const pngUrl = `/api/static/processing/png/${docId}/p-${m[1]}.png`;
const totalPages = idx.total_pages;
// ── Navigation: prev/next page within doc; at boundaries, prev/next document ──
const pp = (n: number) => `p${String(n).padStart(3, "0")}`;
const docIdx = docList.indexOf(docId);
const prevDoc = docIdx > 0 ? docList[docIdx - 1] : null;
const nextDoc = docIdx >= 0 && docIdx < docList.length - 1 ? docList[docIdx + 1] : null;
const prevNav =
pageNum > 1
? { href: `/d/${docId}/${pp(pageNum - 1)}`, label: `← página ${pageNum - 1}`, kind: "page" as const }
: prevDoc
? { href: `/d/${prevDoc}/${pp(1)}`, label: "← documento anterior", kind: "doc" as const }
: null;
const nextNav =
pageNum < totalPages
? { href: `/d/${docId}/${pp(pageNum + 1)}`, label: `página ${pageNum + 1}`, kind: "page" as const }
: nextDoc
? { href: `/d/${nextDoc}/${pp(1)}`, label: "próximo documento →", kind: "doc" as const }
: null;
const navBar = (
<nav className="flex items-center justify-between gap-3 font-mono text-xs">
{prevNav ? (
<Link
href={prevNav.href}
className={`px-3 py-1.5 border rounded hover:bg-[rgba(0,255,156,0.10)] ${
prevNav.kind === "doc"
? "border-[#ffa500] text-[#ffa500] hover:bg-[rgba(255,165,0,0.10)]"
: "border-[rgba(0,255,156,0.30)] text-[#00ff9c]"
}`}
>
{prevNav.label}
</Link>
) : (
<span className="px-3 py-1.5 opacity-30"> início</span>
)}
<span className="text-[#5a6678] tabular-nums">
{pageNum} / {totalPages}
</span>
{nextNav ? (
<Link
href={nextNav.href}
className={`px-3 py-1.5 border rounded hover:bg-[rgba(0,255,156,0.10)] ${
nextNav.kind === "doc"
? "border-[#ffa500] text-[#ffa500] hover:bg-[rgba(255,165,0,0.10)]"
: "border-[rgba(0,255,156,0.30)] text-[#00ff9c]"
}`}
>
{nextNav.label}
</Link>
) : (
<span className="px-3 py-1.5 opacity-30">fim </span>
)}
</nav>
);
return (
<main className="min-h-screen p-6 md:p-10 max-w-6xl mx-auto">
<div className="flex items-start justify-between gap-4 mb-6">
<div className="flex items-center gap-3 font-mono text-xs">
<Link href={`/d/${docId}`} className="text-[#7fdbff] hover:text-[#00ff9c]">
documento inteiro
</Link>
</div>
<AuthBar />
</div>
<header className="mb-6 pb-4 border-b border-[rgba(0,255,156,0.32)]">
<div className="font-mono text-[10px] text-[#5a6678] tracking-widest uppercase mb-2">
página {pageNum} de {totalPages} · {pageChunks.length} trechos · doc:{" "}
<span className="text-[#7fdbff]">{docId}</span>
</div>
<h1 className="font-mono text-xl text-[#00ff9c]">
{(doc?.fm.canonical_title as string) ?? docId} · p{pageNum}
</h1>
</header>
<div className="mb-6">{navBar}</div>
<div className="grid grid-cols-1 lg:grid-cols-[1fr_1fr] gap-8">
<aside className="lg:sticky lg:top-6 lg:self-start lg:max-h-[85vh] lg:overflow-y-auto">
<h2 className="font-mono text-xs uppercase tracking-widest text-[#7fdbff] mb-2">
scanned PNG · 72 DPI
</h2>
<div className="border border-[rgba(0,255,156,0.18)] rounded overflow-hidden bg-[#0a121e]">
<Image
src={pngUrl}
alt={`página ${pageNum}`}
width={800}
height={1100}
sizes="(max-width: 1024px) 100vw, 50vw"
className="block w-full h-auto"
/>
</div>
</aside>
<article>
<h2 className="font-mono text-xs uppercase tracking-widest text-[#7fdbff] mb-2">
trechos (ordem de leitura)
</h2>
{pageChunks.length === 0 ? (
<div className="border border-[rgba(0,255,156,0.15)] bg-[#0a121e] rounded p-6 text-sm text-[#c8d4e6]">
<p className="font-mono text-[#7fdbff] mb-2"> página sem trechos extraídos</p>
<p className="text-[#5a6678] text-xs">
O scan existe (veja à esquerda) mas o processo de chunking não gerou trechos
para esta página específica. Pode ser página em branco, divisor de seção
ou conteúdo sem texto extraível. Próxima execução do chunker preencherá.
</p>
</div>
) : (
<DocRendererV2 docId={docId} chunksByPage={[[pageNum, pageChunks]]} />
)}
</article>
</div>
<div className="mt-10 pt-6 border-t border-[rgba(0,255,156,0.32)]">{navBar}</div>
<ChatBubble context={{ doc_id: docId, page_id: `${docId}/${stem}` }} />
</main>
);
}