/** * Universal frontmatter renderer. * * Introspects ANY entity/doc/page/etc. frontmatter and renders each field with * its semantic UI primitive. The schema (CLAUDE-schema-full.md) IS the contract. * * Grouping strategy (in order): * IDENTITY canonical_name, *_id, aliases, disambiguation_note * CLASSIFICATION highest_classification, classification_markings, language_detected, content_classification * METRICS total_mentions, documents_count, page_count, totals, qualities * RELATIONS primary_*, key_entities, observed_in_event, observers, related, ... * EVIDENCE redactions, signatures_observed, tables_detected, images_detected * TEMPORAL date_start/end, dates, last_* * SPATIAL coordinates, primary_location * ENRICHMENT enrichment_status, external_sources, verified_facts * FLAGS flags */ import { Users, Building2, MapPin, CalendarRange, Disc3, Plane, Crosshair, BookOpen, Files, Image as ImageIcon, Table2, Eye, AlertTriangle, FileSearch, FileWarning, Stamp, PenSquare, Bookmark, Hash, Layers, Zap, } from "lucide-react"; import { FmWikiLink, FmWikiLinkList } from "./fm/wikilink"; import { FmConfidence, FmClassification, FmEnrichmentBadge, FmContentChip, FmPageTypeChip, FmLanguageChip, FmChip, FmStat, FmCoordinates, FmQualityDot, FmFlag, FmTimestamp, } from "./fm/badges"; import { FmBboxThumb } from "./fm/bbox-thumb"; import { FmExternalSources } from "./fm/external-sources"; import type { AnyFrontmatter, EntityRef, EntitiesExtracted } from "@/lib/fm-types"; interface Props { fm: AnyFrontmatter; /** Page-derived context — needed for bbox thumbnails to know which PNG to slice. */ pageCtx?: { docId: string; pageNum: number }; } function isNonEmptyArray(x: unknown): x is unknown[] { return Array.isArray(x) && x.length > 0; } /** Maps a class key under entities_extracted to its proper /e// namespace. */ const ENTITY_NS: Record = { people: { label: "People", ns: "people", icon: }, organizations: { label: "Organizations", ns: "org", icon: }, locations: { label: "Locations", ns: "loc", icon: }, events: { label: "Events", ns: "event", icon: }, uap_objects: { label: "UAP Objects", ns: "uap", icon: }, vehicles: { label: "Vehicles", ns: "vehicle", icon: }, operations: { label: "Operations", ns: "op", icon: }, concepts: { label: "Concepts", ns: "concept", icon: }, }; /** Render an entity ref (extracted from a page) as a wiki-link. * Uses the entity's `name` slugified — best-effort. */ function entityRefToLink(ref: EntityRef, ns: string): string { const name = ref.name ?? ""; if (!name) return ""; const id = name .normalize("NFD") .replace(/[̀-ͯ]/g, "") .toLowerCase() .replace(/[^a-z0-9-]+/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, ""); return `${ns}/${id}`; } function Section({ title, icon, count, children, }: { title: string; icon?: React.ReactNode; count?: number; children: React.ReactNode }) { return (

{icon} {title} {count !== undefined && ({count})}

{children}
); } export function FrontmatterPanel({ fm, pageCtx }: Props) { const sections: React.ReactNode[] = []; /* ── CLASSIFICATION BANNER (top, prominent) ───────────────── */ const markings = fm.classification_markings ?? []; const hasMarkings = markings.length > 0 || fm.highest_classification; if (hasMarkings) { sections.push(
}>
{fm.highest_classification && ( )} {markings.map((m, i) => ( ))}
); } /* ── ALIASES ──────────────────────────────────────────────── */ if (isNonEmptyArray(fm.aliases)) { sections.push(
}>
{(fm.aliases as string[]).slice(0, 16).map((a, i) => ( {a} ))}
); } /* ── DISAMBIGUATION ───────────────────────────────────────── */ if (fm.disambiguation_note) { sections.push(
}>

{fm.disambiguation_note}

); } /* ── CONTENT & PAGE TYPE ──────────────────────────────────── */ const cc = (fm.content_classification ?? []) as string[]; const wantsContent = cc.length > 0 || fm.page_type || fm.language_detected || isNonEmptyArray(fm.languages_detected); if (wantsContent) { sections.push(
}>
{fm.page_type && } {cc.map((k, i) => [0]["kind"]} />)} {fm.language_detected && } {(fm.languages_detected ?? []).map((l, i) => )}
); } /* ── METRICS ──────────────────────────────────────────────── */ const metrics: React.ReactNode[] = []; if (typeof fm.total_mentions === "number") metrics.push(} label="mentions" value={fm.total_mentions} color="cyan" />); if (typeof fm.documents_count === "number") metrics.push(} label="documents" value={fm.documents_count} color="cyan" />); if (typeof fm.page_count === "number") metrics.push(} label="pages" value={fm.page_count} color="amber" />); if (typeof fm.total_redactions === "number" && fm.total_redactions > 0) metrics.push(} label="redactions" value={fm.total_redactions} color="red" />); if (typeof fm.total_signatures === "number" && fm.total_signatures > 0) metrics.push(} label="signatures" value={fm.total_signatures} color="violet" />); if (typeof fm.total_tables === "number" && fm.total_tables > 0) metrics.push(} label="tables" value={fm.total_tables} color="cyan" />); if (typeof fm.total_images === "number" && fm.total_images > 0) metrics.push(} label="images" value={fm.total_images} color="cyan" />); if (typeof fm.ocr_quality_score === "number") metrics.push(} label="ocr" value={`${Math.round(fm.ocr_quality_score * 100)}%`} color="soft" />); if (typeof fm.vision_quality_score === "number") metrics.push(} label="vision" value={`${Math.round(fm.vision_quality_score * 100)}%`} color="soft" />); if (metrics.length > 0) { sections.push(
}>
{metrics}
); } /* ── KEY ENTITIES (document-level rollup) ─────────────────── */ if (fm.key_entities) { const ke = fm.key_entities as EntitiesExtracted; for (const k of Object.keys(ENTITY_NS) as Array) { const refs = ke[k] ?? []; if (!isNonEmptyArray(refs)) continue; const meta = ENTITY_NS[k]; const links = (refs as EntityRef[]).map((r) => entityRefToLink(r, meta.ns)).filter(Boolean); sections.push(
); } } /* ── ENTITIES EXTRACTED (page-level) ──────────────────────── */ if (fm.entities_extracted) { const ee = fm.entities_extracted as EntitiesExtracted; for (const k of Object.keys(ENTITY_NS) as Array) { const refs = ee[k] ?? []; if (!isNonEmptyArray(refs)) continue; const meta = ENTITY_NS[k]; const links = (refs as EntityRef[]).map((r) => entityRefToLink(r, meta.ns)).filter(Boolean); sections.push(
); } } /* ── EVIDENCE: redactions ─────────────────────────────────── */ if (isNonEmptyArray(fm.redactions)) { sections.push(
} count={fm.redactions!.length}>
{fm.redactions!.map((r, i) => (
{r.code ?? "REDACTED"}
{r.description &&
{r.description}
}
))}
); } /* ── EVIDENCE: signatures ─────────────────────────────────── */ if (isNonEmptyArray(fm.signatures_observed)) { sections.push(
} count={fm.signatures_observed!.length}>
{fm.signatures_observed!.map((s, i) => (
{s.signer_inferred ?? "unknown signer"}
{s.notes &&
{s.notes}
}
))}
); } /* ── EVIDENCE: tables ─────────────────────────────────────── */ if (isNonEmptyArray(fm.tables_detected)) { sections.push(
} count={fm.tables_detected!.length}>
{fm.tables_detected!.map((t, i) => (
{t.table_id ? : inline table}
{t.col_count_estimate ?? "?"}×{t.row_count_estimate ?? "?"} {t.spans_multi_page && multi-page}
{t.headers_summary &&
{t.headers_summary}
}
))}
); } /* ── EVIDENCE: images detected ────────────────────────────── */ if (isNonEmptyArray(fm.images_detected)) { sections.push(
} count={fm.images_detected!.length}>
{fm.images_detected!.map((im, i) => (
{im.image_type ?? "image"}
{im.caption_ocr &&
{im.caption_ocr}
}
))}
); } /* ── UAP OBSERVATION ──────────────────────────────────────── */ if (fm.uap_observation_fields) { const u = fm.uap_observation_fields; sections.push(
}>
{u.shape && } label={`shape: ${u.shape}`} />} {u.color && } {u.size_estimate && } {u.altitude_ft !== null && u.altitude_ft !== undefined && } {u.speed_kts !== null && u.speed_kts !== undefined && } {u.bearing_deg !== null && u.bearing_deg !== undefined && } {u.distance_nm !== null && u.distance_nm !== undefined && } {u.duration_seconds && } {u.coordinates && }
); } /* ── UAP-OBJECT specific fields (entity-level) ────────────── */ if (fm.entity_class === "uap_object") { const u = fm; sections.push(
}>
{u.shape && } {u.color && } {u.size_estimate_m && } {u.altitude_ft && } {u.speed_kts && } {isNonEmptyArray(u.maneuver_descriptors) && u.maneuver_descriptors!.map((m, i) => )} {isNonEmptyArray(u.features) && u.features!.map((f, i) => )} {u.confidence_band_overall && }
{u.observed_in_event && (
observed in:
)}
); } /* ── LOCATION specific ────────────────────────────────────── */ if (fm.entity_class === "location") { sections.push(
}>
{fm.location_type && } {fm.country && } {fm.region && } {fm.coordinates && ( )} {fm.parent_location && }
); } /* ── EVENT specific ───────────────────────────────────────── */ if (fm.entity_class === "event") { sections.push(
}>
{fm.event_class && } {fm.date_start && } {fm.date_end && fm.date_end !== fm.date_start && } {fm.date_confidence && } {fm.primary_location && } {isNonEmptyArray(fm.observers) && fm.observers!.map((o, i) => )} {isNonEmptyArray(fm.uap_objects) && fm.uap_objects!.map((u, i) => )}
); } /* ── PERSON specific ──────────────────────────────────────── */ if (fm.entity_class === "person") { const dates = fm.dates ?? {}; sections.push(
}>
{fm.primary_role && } {fm.primary_organization && } {isNonEmptyArray(fm.roles) && fm.roles!.map((r, i) => )} {(dates.born || dates.died) && ( )}
); } /* ── ORGANIZATION specific ────────────────────────────────── */ if (fm.entity_class === "organization") { sections.push(
}>
{fm.organization_type && } {fm.country && } {fm.founded && }
); } /* ── CONCEPT/VEHICLE/OPERATION quick chips ────────────────── */ if (fm.entity_class === "concept") { sections.push(
}>
{fm.concept_class && } {fm.domain && }
{fm.definition_short &&

{fm.definition_short}

} {fm.definition_short_pt_br &&

{fm.definition_short_pt_br}

}
); } /* ── ENRICHMENT ───────────────────────────────────────────── */ if (fm.enrichment_status || isNonEmptyArray(fm.external_sources)) { sections.push(
}>
{fm.last_enriched && }
); } /* ── FLAGS ────────────────────────────────────────────────── */ if (isNonEmptyArray(fm.flags)) { sections.push(
}>
{fm.flags!.map((f, i) => )}
); } /* ── TIMESTAMPS (compact footer) ──────────────────────────── */ const ts: React.ReactNode[] = []; if (fm.last_ingest) ts.push(); if (fm.last_lint) ts.push(); if (fm.last_enriched && !fm.external_sources) ts.push(); return (
{sections} {ts.length > 0 && (
{ts}
)}
); }