disclosure-bureau/web/components/reader-content.tsx

128 lines
4 KiB
TypeScript
Raw Normal View History

"use client";
import { useState } from "react";
import { EntityModal } from "./entity-modal";
import { FmBboxThumb } from "./fm/bbox-thumb";
import { FmChip } from "./fm/badges";
export interface Match {
entity_id: string;
class: string;
alias_matched: string;
start: number;
end: number;
}
interface ReaderContentProps {
ocr: string;
matches: Match[];
imagesDetected?: Array<{
bbox?: { x: number; y: number; w: number; h: number };
image_type?: string;
caption_ocr?: string;
local_image_index?: number;
}>;
docId: string;
pageNum: number;
}
interface Segment {
text: string;
match?: Match;
}
function segmentText(text: string, matches: Match[]): Segment[] {
if (!matches || matches.length === 0) return [{ text }];
const sorted = [...matches].sort((a, b) => a.start - b.start);
const segs: Segment[] = [];
let cursor = 0;
for (const m of sorted) {
if (m.start < cursor) continue;
if (m.start > cursor) segs.push({ text: text.slice(cursor, m.start) });
segs.push({ text: text.slice(m.start, m.end), match: m });
cursor = m.end;
}
if (cursor < text.length) segs.push({ text: text.slice(cursor) });
return segs;
}
export function ReaderContent({ ocr, matches, imagesDetected = [], docId, pageNum }: ReaderContentProps) {
const [modalEntity, setModalEntity] = useState<{ cls: string; id: string } | null>(null);
const segments = segmentText(ocr, matches);
return (
<>
<pre className="font-sans">
{segments.map((seg, i) =>
seg.match ? (
<span
key={i}
className="entity-link"
data-class={seg.match.class}
data-entity-id={seg.match.entity_id}
onClick={() => setModalEntity({ cls: seg.match!.class, id: seg.match!.entity_id })}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
setModalEntity({ cls: seg.match!.class, id: seg.match!.entity_id });
}
}}
title={`${seg.match.class}/${seg.match.entity_id}`}
>
{seg.text}
</span>
) : (
<span key={i}>{seg.text}</span>
),
)}
</pre>
{imagesDetected.length > 0 && (
<section className="mt-8">
<h3 className="font-mono text-[11px] text-[#8896aa] uppercase tracking-widest mb-3">
Detected images on this page ({imagesDetected.length})
</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
{imagesDetected.map((img, i) => (
<figure key={i} className="m-0">
<FmBboxThumb
bbox={img.bbox}
docId={docId}
pageNum={pageNum}
width={180}
height={180}
label={img.caption_ocr}
/>
<figcaption className="mt-1 flex flex-wrap items-center gap-1 font-mono text-[10px] text-[#8896aa]">
<FmChip
color={img.image_type === "sketch" ? "amber"
: img.image_type === "photo" ? "cyan"
: img.image_type === "map" ? "green"
: img.image_type === "stamp" ? "violet"
: img.image_type === "signature" ? "violet"
: "soft"}
label={img.image_type ?? "image"}
/>
{img.caption_ocr && (
<span className="text-[#c8d4e6] line-clamp-2">{img.caption_ocr}</span>
)}
</figcaption>
</figure>
))}
</div>
</section>
)}
{modalEntity && (
<EntityModal
cls={modalEntity.cls}
id={modalEntity.id}
open={!!modalEntity}
onClose={() => setModalEntity(null)}
/>
)}
</>
);
}