2026-05-18 01:44:36 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* DocRendererV2 — render a document from agentic chunks (raw/<doc>--subagent/).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Per chunk, picks the right HTML element from its type, places images at
|
|
|
|
|
|
* their bbox position via on-demand /api/crop, and renders tables when
|
|
|
|
|
|
* a related_table is attached.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Modes:
|
|
|
|
|
|
* - flow: continuous reading order (one column, all pages)
|
|
|
|
|
|
* - paged: page-by-page, with mini PNG thumbnail per page
|
|
|
|
|
|
*/
|
|
|
|
|
|
"use client";
|
|
|
|
|
|
import { useState } from "react";
|
|
|
|
|
|
import Image from "next/image";
|
|
|
|
|
|
import type { ParsedChunk } from "@/lib/chunks";
|
|
|
|
|
|
|
|
|
|
|
|
type Mode = "flow" | "paged";
|
|
|
|
|
|
type Lang = "pt-br" | "en" | "both";
|
|
|
|
|
|
|
|
|
|
|
|
const CSS_VARS = {
|
|
|
|
|
|
cyan: "#7fdbff",
|
|
|
|
|
|
green: "#00ff9c",
|
|
|
|
|
|
dim: "#5a6678",
|
|
|
|
|
|
text: "#c8d4e6",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Chunks that are pure visual noise on a scanned page — skip rendering entirely.
|
|
|
|
|
|
const NOISE_CHUNK_TYPES = new Set<string>([
|
|
|
|
|
|
"blank",
|
|
|
|
|
|
"blank_area",
|
|
|
|
|
|
"blank_page",
|
|
|
|
|
|
"separator",
|
|
|
|
|
|
"punch_hole",
|
|
|
|
|
|
"fastener_hole",
|
|
|
|
|
|
"barcode",
|
|
|
|
|
|
"redaction_bar",
|
|
|
|
|
|
"redaction_header",
|
|
|
|
|
|
"redaction_footer",
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// ALLOWLIST: only render an image crop when image_type is something the
|
|
|
|
|
|
// reader actually benefits from seeing — photographs, drawings, maps,
|
|
|
|
|
|
// illustrations, newspaper clippings, UAP-object crops, sensor frames.
|
|
|
|
|
|
// Everything else (seals, stamps, signatures, labels, envelopes, page
|
|
|
|
|
|
// edges, holes, marks, watermarks, etc.) is filtered out — it adds no
|
|
|
|
|
|
// investigative value and clutters the page.
|
|
|
|
|
|
const RENDERABLE_IMAGE_TYPES = new Set<string>([
|
|
|
|
|
|
// Photographs (all flavors)
|
|
|
|
|
|
"photo",
|
|
|
|
|
|
"photograph",
|
|
|
|
|
|
"photograph_of_document",
|
|
|
|
|
|
"ufo_photograph",
|
|
|
|
|
|
"surveillance_photo",
|
|
|
|
|
|
"surveillance_photograph",
|
|
|
|
|
|
"surveillance_infrared_photo",
|
|
|
|
|
|
"infrared_photo",
|
|
|
|
|
|
"infrared_photo_detail",
|
|
|
|
|
|
"thermal_infrared_photo",
|
|
|
|
|
|
"thermal_infrared_surveillance_frame",
|
|
|
|
|
|
"aerial_surveillance",
|
|
|
|
|
|
"portrait",
|
|
|
|
|
|
// Drawings & sketches
|
|
|
|
|
|
"drawing",
|
|
|
|
|
|
"sketch",
|
|
|
|
|
|
"hand_drawn_diagram",
|
|
|
|
|
|
"doodle",
|
|
|
|
|
|
"artist_rendering",
|
|
|
|
|
|
// Diagrams & maps
|
|
|
|
|
|
"diagram",
|
|
|
|
|
|
"map",
|
|
|
|
|
|
"map_diagram",
|
|
|
|
|
|
"geographical_map",
|
|
|
|
|
|
"anatomical_diagram",
|
|
|
|
|
|
"ufo_diagram_technical",
|
|
|
|
|
|
// Illustrations
|
|
|
|
|
|
"illustration",
|
|
|
|
|
|
"illustration_ufo_encounter",
|
|
|
|
|
|
"illustration_ufo_sighting",
|
|
|
|
|
|
"cartoon_illustration",
|
|
|
|
|
|
"composite_rendering",
|
|
|
|
|
|
// Newspaper / magazine clippings (informational content)
|
|
|
|
|
|
"newspaper_clipping",
|
|
|
|
|
|
"newspaper_article_eyewitness",
|
|
|
|
|
|
"newspaper_article_with_headline",
|
|
|
|
|
|
"newspaper_article_mass_sighting",
|
|
|
|
|
|
"newspaper_article_regional_sighting",
|
|
|
|
|
|
"newspaper_clipping_collage",
|
|
|
|
|
|
"newspaper_clipping_composite",
|
|
|
|
|
|
"newspaper_clippings_display",
|
|
|
|
|
|
"newspaper_collage",
|
|
|
|
|
|
"magazine_page",
|
|
|
|
|
|
"magazine_cover",
|
|
|
|
|
|
"advertisement_illustration",
|
|
|
|
|
|
"clipping",
|
|
|
|
|
|
// UAP object crops
|
|
|
|
|
|
"uap_object",
|
|
|
|
|
|
"uap_object_crop",
|
|
|
|
|
|
"uap_object_detail",
|
|
|
|
|
|
"uap_detail_crop",
|
|
|
|
|
|
"uap_closeup",
|
|
|
|
|
|
"aerial_object",
|
|
|
|
|
|
"aerial_objects_closeup",
|
|
|
|
|
|
"sensor_frame",
|
|
|
|
|
|
"sensor_footage",
|
|
|
|
|
|
"sensor_overlay",
|
|
|
|
|
|
"sensor_reticle_with_uap",
|
|
|
|
|
|
"thermal_sensor_frame",
|
|
|
|
|
|
"infrared_camera_frame",
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
function ChunkCard({
|
|
|
|
|
|
c,
|
|
|
|
|
|
lang,
|
|
|
|
|
|
docId,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
c: ParsedChunk;
|
|
|
|
|
|
lang: Lang;
|
|
|
|
|
|
docId: string;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const { fm, content_en, content_pt } = c;
|
|
|
|
|
|
const bbox = fm.bbox ?? { x: 0, y: 0, w: 1, h: 0.05 };
|
|
|
|
|
|
const showEn = lang === "en" || lang === "both";
|
|
|
|
|
|
const showPt = lang === "pt-br" || lang === "both";
|
|
|
|
|
|
|
|
|
|
|
|
// Skip pure visual noise (blank areas, binder holes, separators, bare redaction bars)
|
|
|
|
|
|
if (typeof fm.type === "string" && NOISE_CHUNK_TYPES.has(fm.type)) return null;
|
|
|
|
|
|
|
|
|
|
|
|
// Image chunks: only render the crop if image_type is on the allowlist
|
|
|
|
|
|
// (photographs, drawings, maps, diagrams, illustrations, clippings, UAP
|
|
|
|
|
|
// crops, sensor frames). Seals, stamps, signatures, labels, envelopes,
|
|
|
|
|
|
// marks, holes, etc. produce visually meaningless crops — drop them.
|
|
|
|
|
|
if (fm.type === "image" || fm.image_type) {
|
|
|
|
|
|
const it = typeof fm.image_type === "string" ? fm.image_type : "";
|
|
|
|
|
|
if (!RENDERABLE_IMAGE_TYPES.has(it)) return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Anchor for citation jumps
|
|
|
|
|
|
const anchor = (
|
|
|
|
|
|
<a id={fm.chunk_id} className="absolute -mt-24" aria-hidden="true" />
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Image chunks: render crop inline
|
|
|
|
|
|
if (fm.type === "image" || fm.image_type) {
|
2026-05-21 19:40:51 +00:00
|
|
|
|
const hasValidBbox =
|
|
|
|
|
|
bbox &&
|
|
|
|
|
|
typeof bbox.w === "number" && typeof bbox.h === "number" &&
|
|
|
|
|
|
typeof bbox.x === "number" && typeof bbox.y === "number" &&
|
|
|
|
|
|
bbox.w > 0 && bbox.h > 0;
|
|
|
|
|
|
|
|
|
|
|
|
// No crop coordinates → don't request a broken crop (400). Render the
|
|
|
|
|
|
// image's textual description so the content is still surfaced.
|
|
|
|
|
|
if (!hasValidBbox) {
|
|
|
|
|
|
const descEn = fm.image_description_en || content_en;
|
|
|
|
|
|
const descPt = fm.image_description_pt_br || content_pt;
|
|
|
|
|
|
if (!descEn && !descPt) return null;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<figure className="relative my-8" id={fm.chunk_id}>
|
|
|
|
|
|
{anchor}
|
|
|
|
|
|
<div className="border border-[rgba(0,255,156,0.20)] bg-[#0a121e] rounded p-4 text-sm text-[#c8d4e6]">
|
|
|
|
|
|
<div className="font-mono text-[10px] text-[#7fdbff] mb-2">
|
|
|
|
|
|
🖼 {fm.chunk_id} · p{fm.page}
|
|
|
|
|
|
{fm.image_type ? ` · ${fm.image_type}` : ""} · descrição (sem recorte)
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{showEn && descEn && <p className="mb-1"><b>EN:</b> {descEn}</p>}
|
|
|
|
|
|
{showPt && descPt && <p><b>PT-BR:</b> {descPt}</p>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</figure>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 01:44:36 +00:00
|
|
|
|
const cropUrl =
|
|
|
|
|
|
`/api/crop?doc=${encodeURIComponent(docId)}` +
|
|
|
|
|
|
`&page=${fm.page}&x=${bbox.x}&y=${bbox.y}&w=${bbox.w}&h=${bbox.h}&w_px=800`;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<figure className="relative my-8" id={fm.chunk_id}>
|
|
|
|
|
|
{anchor}
|
|
|
|
|
|
<div className="relative border border-[rgba(0,255,156,0.20)] bg-[#0a121e] rounded overflow-hidden">
|
|
|
|
|
|
<Image
|
|
|
|
|
|
src={cropUrl}
|
|
|
|
|
|
alt={(showPt ? content_pt : content_en) || `chunk ${fm.chunk_id}`}
|
|
|
|
|
|
width={800}
|
|
|
|
|
|
height={Math.round(800 * (bbox.h / Math.max(0.01, bbox.w)))}
|
|
|
|
|
|
sizes="(max-width: 768px) 90vw, 700px"
|
|
|
|
|
|
className="block w-full h-auto"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<figcaption className="mt-2 text-xs text-[#8896aa] flex items-center gap-2">
|
|
|
|
|
|
<span className="font-mono text-[#7fdbff]">{fm.chunk_id}</span>
|
|
|
|
|
|
<span>·</span>
|
|
|
|
|
|
<span>p{fm.page}</span>
|
|
|
|
|
|
{fm.image_type && <span>· {fm.image_type}</span>}
|
|
|
|
|
|
{fm.ufo_anomaly_detected && (
|
2026-05-21 19:42:37 +00:00
|
|
|
|
<span className="text-[#00ff9c]">· 🛸 UAP: {(typeof fm.ufo_anomaly_type === "string" && fm.ufo_anomaly_type.trim() && fm.ufo_anomaly_type.toLowerCase() !== "null") ? fm.ufo_anomaly_type : "anomalia"}</span>
|
2026-05-18 01:44:36 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</figcaption>
|
|
|
|
|
|
{(showEn || showPt) && (
|
|
|
|
|
|
<div className="mt-2 text-xs text-[#8896aa] space-y-1">
|
|
|
|
|
|
{showEn && fm.image_description_en && <p><b>EN:</b> {fm.image_description_en}</p>}
|
|
|
|
|
|
{showPt && fm.image_description_pt_br && <p><b>PT-BR:</b> {fm.image_description_pt_br}</p>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</figure>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Classification banner
|
|
|
|
|
|
if (fm.type === "classification_marking" || fm.type === "classification_banner") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="my-4 text-center" id={fm.chunk_id}>
|
|
|
|
|
|
{anchor}
|
|
|
|
|
|
<span className="inline-block font-mono text-xs px-3 py-1 border border-[#ff6b6b] text-[#ff6b6b] bg-[rgba(255,107,107,0.05)]">
|
|
|
|
|
|
{showPt ? content_pt : content_en}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Stamp
|
|
|
|
|
|
if (fm.type === "stamp") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="my-3 inline-block mr-2" id={fm.chunk_id}>
|
|
|
|
|
|
{anchor}
|
|
|
|
|
|
<span
|
|
|
|
|
|
className="font-mono text-[10px] px-2 py-1 border-2 border-[#a78bfa] text-[#a78bfa] rounded"
|
|
|
|
|
|
style={{ transform: "rotate(-2deg)" }}
|
|
|
|
|
|
>
|
|
|
|
|
|
🏛️ {showPt ? content_pt : content_en}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Signature
|
|
|
|
|
|
if (fm.type === "signature" || fm.type === "signature_block") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="my-3 italic text-[#8896aa] font-serif" id={fm.chunk_id}>
|
|
|
|
|
|
{anchor}
|
|
|
|
|
|
✒️ {showPt ? content_pt : content_en}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Letterhead
|
|
|
|
|
|
if (fm.type === "letterhead") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<header className="my-6 text-center border-b border-[rgba(0,255,156,0.20)] pb-3" id={fm.chunk_id}>
|
|
|
|
|
|
{anchor}
|
|
|
|
|
|
{showEn && <div className="font-mono text-sm uppercase tracking-wider text-[#7fdbff]">{content_en}</div>}
|
|
|
|
|
|
{showPt && lang === "both" && (
|
|
|
|
|
|
<div className="font-mono text-xs uppercase tracking-wider text-[#8896aa] mt-1">{content_pt}</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{!showEn && showPt && <div className="font-mono text-sm uppercase tracking-wider text-[#7fdbff]">{content_pt}</div>}
|
|
|
|
|
|
</header>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Address block
|
|
|
|
|
|
if (fm.type === "address_block" || fm.type === "addressee_block") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<address className="my-4 not-italic font-mono text-sm text-[#c8d4e6] whitespace-pre-line" id={fm.chunk_id}>
|
|
|
|
|
|
{anchor}
|
|
|
|
|
|
{showEn && <div>{content_en}</div>}
|
|
|
|
|
|
{showPt && lang === "both" && <div className="text-[#8896aa] mt-1">{content_pt}</div>}
|
|
|
|
|
|
{!showEn && showPt && <div>{content_pt}</div>}
|
|
|
|
|
|
</address>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Heading
|
|
|
|
|
|
if (fm.type === "heading" || fm.type === "header_block" || fm.type === "subject_line" || fm.type === "section_header") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<h3 className="mt-8 mb-3 text-lg font-mono text-[#00ff9c] border-l-2 border-[#00ff9c] pl-3" id={fm.chunk_id}>
|
|
|
|
|
|
{anchor}
|
|
|
|
|
|
{showPt ? content_pt : content_en}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Footer
|
|
|
|
|
|
if (fm.type === "footer") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<footer className="my-4 text-xs text-[#5a6678] font-mono text-center" id={fm.chunk_id}>
|
|
|
|
|
|
{anchor}
|
|
|
|
|
|
{showPt ? content_pt : content_en}
|
|
|
|
|
|
</footer>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Form field
|
|
|
|
|
|
if (fm.type === "form_field" || fm.type === "form_reference") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="my-2 font-mono text-sm" id={fm.chunk_id}>
|
|
|
|
|
|
{anchor}
|
|
|
|
|
|
<span className="text-[#7fdbff]">▸</span> {showPt ? content_pt : content_en}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Bulleted / numbered
|
|
|
|
|
|
if (fm.type === "bulleted_item") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<li className="my-1 ml-6" id={fm.chunk_id}>
|
|
|
|
|
|
{anchor}
|
|
|
|
|
|
{showPt ? content_pt : content_en}
|
|
|
|
|
|
</li>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (fm.type === "numbered_item") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<li className="my-1 ml-6 list-decimal" id={fm.chunk_id}>
|
|
|
|
|
|
{anchor}
|
|
|
|
|
|
{showPt ? content_pt : content_en}
|
|
|
|
|
|
</li>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Quote
|
|
|
|
|
|
if (fm.type === "quote_block") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<blockquote className="my-4 ml-4 pl-4 border-l-4 border-[#7fdbff] italic text-[#c8d4e6]" id={fm.chunk_id}>
|
|
|
|
|
|
{anchor}
|
|
|
|
|
|
{showPt ? content_pt : content_en}
|
|
|
|
|
|
</blockquote>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Marginalia
|
|
|
|
|
|
if (fm.type === "marginalia") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<aside className="my-3 ml-8 text-xs text-[#a78bfa] font-handwriting italic" id={fm.chunk_id}>
|
|
|
|
|
|
{anchor}
|
|
|
|
|
|
✍️ {showPt ? content_pt : content_en}
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Redaction
|
|
|
|
|
|
if (fm.type === "redaction") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span
|
|
|
|
|
|
className="my-2 inline-block px-3 py-1 bg-black text-[#5a6678] font-mono text-xs rounded"
|
|
|
|
|
|
id={fm.chunk_id}
|
|
|
|
|
|
title={fm.redaction_code ?? "redacted"}
|
|
|
|
|
|
>
|
|
|
|
|
|
{anchor}
|
|
|
|
|
|
▓▓▓ [REDACTED{fm.redaction_code ? ` — ${fm.redaction_code}` : ""}]
|
|
|
|
|
|
</span>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Table marker (basic — full table render needs related_table data)
|
|
|
|
|
|
if (fm.type === "table_marker" && fm.related_table) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="my-6 border border-[rgba(127,219,255,0.30)] bg-[#0a121e] p-4 rounded" id={fm.chunk_id}>
|
|
|
|
|
|
{anchor}
|
|
|
|
|
|
<div className="text-xs font-mono text-[#7fdbff] mb-2">📊 {fm.related_table}</div>
|
|
|
|
|
|
<div className="text-sm text-[#c8d4e6]">{showPt ? content_pt : content_en}</div>
|
|
|
|
|
|
<a
|
|
|
|
|
|
href={`/api/static/raw/${docId}--subagent/tables/${fm.related_table}.csv`}
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noopener"
|
|
|
|
|
|
className="mt-2 inline-block text-xs text-[#00ff9c] hover:underline font-mono"
|
|
|
|
|
|
>
|
|
|
|
|
|
download CSV →
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Paragraph (default)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="my-3 text-[15px] leading-relaxed text-[#c8d4e6]" id={fm.chunk_id}>
|
|
|
|
|
|
{anchor}
|
|
|
|
|
|
{showEn && <p>{content_en}</p>}
|
|
|
|
|
|
{showPt && lang === "both" && <p className="mt-1 text-[#8896aa] text-sm">{content_pt}</p>}
|
|
|
|
|
|
{!showEn && showPt && <p>{content_pt}</p>}
|
2026-05-21 19:42:37 +00:00
|
|
|
|
{fm.ufo_anomaly_detected && <UapFlag fm={fm} />}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** UAP anomaly flag — shows type and/or rationale, omitting parts that are
|
|
|
|
|
|
* absent so an uncharacterized flag doesn't render a dangling "anomaly —". */
|
|
|
|
|
|
function UapFlag({ fm }: { fm: ParsedChunk["fm"] }) {
|
|
|
|
|
|
const clean = (v: unknown) => {
|
|
|
|
|
|
const s = typeof v === "string" ? v.trim() : "";
|
|
|
|
|
|
return s && s.toLowerCase() !== "null" ? s : null;
|
|
|
|
|
|
};
|
|
|
|
|
|
const type = clean(fm.ufo_anomaly_type);
|
|
|
|
|
|
const rationale = clean(fm.ufo_anomaly_rationale);
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mt-1 text-xs text-[#00ff9c] font-mono">
|
|
|
|
|
|
🛸 UAP flag: {type ?? "anomalia"}
|
|
|
|
|
|
{rationale ? ` — ${rationale}` : ""}
|
2026-05-18 01:44:36 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isNoiseChunk(c: ParsedChunk): boolean {
|
|
|
|
|
|
const t = c.fm.type;
|
|
|
|
|
|
const it = c.fm.image_type;
|
|
|
|
|
|
if (typeof t === "string" && NOISE_CHUNK_TYPES.has(t)) return true;
|
|
|
|
|
|
// For chunks that have an image_type but ARE image-class (type === "image"),
|
|
|
|
|
|
// hide unless they're on the allowlist. Non-image chunks with a stray
|
|
|
|
|
|
// image_type field (e.g. type "stamp" with image_type "stamp") are also
|
|
|
|
|
|
// skipped — we don't render visual crops for them.
|
|
|
|
|
|
const isImageChunk = t === "image" || typeof it === "string";
|
|
|
|
|
|
if (isImageChunk && !(typeof it === "string" && RENDERABLE_IMAGE_TYPES.has(it))) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function PageGroup({
|
|
|
|
|
|
page,
|
|
|
|
|
|
chunks,
|
|
|
|
|
|
lang,
|
|
|
|
|
|
docId,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
page: number;
|
|
|
|
|
|
chunks: ParsedChunk[];
|
|
|
|
|
|
lang: Lang;
|
|
|
|
|
|
docId: string;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const visibleChunks = chunks.filter((c) => !isNoiseChunk(c));
|
|
|
|
|
|
if (visibleChunks.length === 0) return null;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<section className="mb-12">
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="sticky top-0 z-10 bg-[#040810] border-b border-[rgba(0,255,156,0.15)] py-2 mb-4 flex items-center justify-between"
|
|
|
|
|
|
>
|
|
|
|
|
|
<h2 className="font-mono text-xs uppercase tracking-widest text-[#5a6678]">
|
|
|
|
|
|
▍ página <span className="text-[#7fdbff]">{page}</span> — {visibleChunks.length} trechos
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<a
|
|
|
|
|
|
href={`/d/${docId}/p${String(page).padStart(3, "0")}`}
|
|
|
|
|
|
className="font-mono text-[10px] text-[#7fdbff] hover:text-[#00ff9c]"
|
|
|
|
|
|
>
|
|
|
|
|
|
ver scan original →
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{visibleChunks.map((c) => (
|
|
|
|
|
|
<ChunkCard key={c.fm.chunk_id} c={c} lang={lang} docId={docId} />
|
|
|
|
|
|
))}
|
|
|
|
|
|
</section>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function DocRendererV2({
|
|
|
|
|
|
docId,
|
|
|
|
|
|
chunksByPage,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
docId: string;
|
|
|
|
|
|
chunksByPage: Array<[number, ParsedChunk[]]>;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const [lang, setLang] = useState<Lang>("pt-br");
|
|
|
|
|
|
const [mode, setMode] = useState<Mode>("paged");
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="mb-6 flex items-center justify-between flex-wrap gap-3">
|
|
|
|
|
|
<div className="flex items-center gap-2 font-mono text-xs">
|
|
|
|
|
|
{(["pt-br", "en", "both"] as Lang[]).map((l) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={l}
|
|
|
|
|
|
onClick={() => setLang(l)}
|
|
|
|
|
|
className={`px-3 py-1.5 border rounded ${
|
|
|
|
|
|
lang === l
|
|
|
|
|
|
? "border-[#00ff9c] text-[#00ff9c] bg-[rgba(0,255,156,0.08)]"
|
|
|
|
|
|
: "border-[rgba(0,255,156,0.20)] text-[#8896aa]"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{l}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-2 font-mono text-xs">
|
|
|
|
|
|
{(["paged", "flow"] as Mode[]).map((m) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={m}
|
|
|
|
|
|
onClick={() => setMode(m)}
|
|
|
|
|
|
className={`px-3 py-1.5 border rounded ${
|
|
|
|
|
|
mode === m
|
|
|
|
|
|
? "border-[#7fdbff] text-[#7fdbff] bg-[rgba(127,219,255,0.08)]"
|
|
|
|
|
|
: "border-[rgba(127,219,255,0.20)] text-[#8896aa]"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{m === "paged" ? "paged" : "flow"}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{mode === "paged" ? (
|
|
|
|
|
|
chunksByPage.map(([page, chunks]) => (
|
|
|
|
|
|
<PageGroup key={page} page={page} chunks={chunks} lang={lang} docId={docId} />
|
|
|
|
|
|
))
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
{chunksByPage.flatMap(([, chunks]) =>
|
|
|
|
|
|
chunks.map((c) => <ChunkCard key={c.fm.chunk_id} c={c} lang={lang} docId={docId} />),
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|