/** * InlineCitation — renders a [[doc/p007#c0042]] citation as a clickable mini-card * with bbox thumbnail, page, type, classification, and snippet. Fetches chunk * data lazily from /api/chunk on first render. * * Used by: * - to upgrade chunk-anchor wiki-links (in chat assistant messages * and in any markdown body that cites chunks) */ "use client"; import { useEffect, useState } from "react"; import Link from "next/link"; interface ChunkData { chunk_id: string; doc_id: string; page: number; type: string; bbox: { x: number; y: number; w: number; h: number } | null; classification: string | null; content_en: string | null; content_pt: string | null; } const cache = new Map(); export function InlineCitation({ docId, chunkId, label, lang = "pt", }: { docId: string; chunkId: string; label?: string; lang?: "pt" | "en"; }) { const key = `${docId}/${chunkId}`; const cached = cache.get(key); const [data, setData] = useState( cached && cached !== "not_found" ? cached : null, ); const [missing, setMissing] = useState(cached === "not_found"); const [open, setOpen] = useState(false); useEffect(() => { if (cached) return; let cancelled = false; (async () => { try { const res = await fetch( `/api/chunk?doc=${encodeURIComponent(docId)}&chunk=${encodeURIComponent(chunkId)}`, ); if (!res.ok) { cache.set(key, "not_found"); if (!cancelled) setMissing(true); return; } const payload = (await res.json()) as ChunkData; cache.set(key, payload); if (!cancelled) setData(payload); } catch { cache.set(key, "not_found"); if (!cancelled) setMissing(true); } })(); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [docId, chunkId]); const href = `/d/${docId}#${chunkId}`; if (missing) { return ( {label ?? chunkId} ); } if (!data) { return ( · {label ?? chunkId} ); } const text = lang === "en" ? data.content_en || data.content_pt : data.content_pt || data.content_en; const cropUrl = data.bbox ? `/api/crop?doc=${encodeURIComponent(docId)}&page=${data.page}` + `&x=${data.bbox.x}&y=${data.bbox.y}&w=${data.bbox.w}&h=${data.bbox.h}&w_px=320` : null; return ( {open && ( {data.chunk_id} p{data.page} {data.type} {data.classification && ( {data.classification} )} {cropUrl && ( {/* eslint-disable-next-line @next/next/no-img-element */} )} {text && ( {text} )} abrir página inteira → )} ); }