128 lines
4 KiB
TypeScript
128 lines
4 KiB
TypeScript
|
|
"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)}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|