feat(kb-ui): humanise provenance "Added by" via an injected resolver

ProvenancePanel/ObjectViewer gain an optional resolvePrincipal(p) prop so the
consumer can turn a {type,id} principal ref into a human label — the lib can't
know who "you" is. Falls back to the raw type·id when the resolver returns
undefined, so existing callers are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jules
2026-07-11 09:03:35 +10:00
parent efc2c18a91
commit 7997dbb3f3

View File

@@ -16,7 +16,7 @@ import {
Tag, Tag,
Trash2, Trash2,
} from "lucide-react"; } from "lucide-react";
import type { ObjectOutline, ObjectSummary, OutlineSection, TextSlice } from "./types"; import type { ObjectOutline, ObjectSummary, OutlineSection, PrincipalRef, TextSlice } from "./types";
import { import {
Badge, Badge,
SensitivityBadge, SensitivityBadge,
@@ -160,11 +160,20 @@ export const CatalogCard: FC<{ outline: ObjectOutline; className?: string }> = (
// ---- ProvenancePanel ------------------------------------------------------ // ---- ProvenancePanel ------------------------------------------------------
export const ProvenancePanel: FC<{ outline: ObjectOutline; className?: string }> = ({ outline, className }) => { export const ProvenancePanel: FC<{
outline: ObjectOutline;
/** Turn a principal ref into a human label ("You", an agent's name, …). The
* lib can't know who "you" is, so the app supplies this; falls back to the
* raw type·id when it returns undefined. */
resolvePrincipal?: (p: string | PrincipalRef) => string | undefined;
className?: string;
}> = ({ outline, resolvePrincipal, className }) => {
const p = outline.provenance; const p = outline.provenance;
const addedBy =
(p.created_by != null ? resolvePrincipal?.(p.created_by) : undefined) ?? formatPrincipal(p.created_by) ?? "—";
const rows: [string, ReactNode][] = [ const rows: [string, ReactNode][] = [
["Source", p.source_ref ? `${p.source_type} · ${p.source_ref}` : p.source_type], ["Source", p.source_ref ? `${p.source_type} · ${p.source_ref}` : p.source_type],
["Added by", formatPrincipal(p.created_by) ?? "—"], ["Added by", addedBy],
["Added", formatDate(p.inserted_at)], ["Added", formatDate(p.inserted_at)],
["Effective", p.effective_at ? formatDate(p.effective_at) : "—"], ["Effective", p.effective_at ? formatDate(p.effective_at) : "—"],
["Last verified", p.verified_at ? formatDate(p.verified_at) : "Not confirmed"], ["Last verified", p.verified_at ? formatDate(p.verified_at) : "Not confirmed"],
@@ -499,6 +508,8 @@ export interface ObjectViewerProps {
readText: TextReaderProps["readText"]; readText: TextReaderProps["readText"];
/** Resolve a browser-openable blob URL (for images + download). */ /** Resolve a browser-openable blob URL (for images + download). */
resolveBlobUrl?: () => Promise<string>; resolveBlobUrl?: () => Promise<string>;
/** Humanise the "Added by" principal (see ProvenancePanel). */
resolvePrincipal?: (p: string | PrincipalRef) => string | undefined;
highlight?: { start: number; end: number }; highlight?: { start: number; end: number };
onOpenObject?: (objectId: string, span?: { start: number; end: number }) => void; onOpenObject?: (objectId: string, span?: { start: number; end: number }) => void;
actions?: ObjectActions; actions?: ObjectActions;
@@ -509,6 +520,7 @@ export const ObjectViewer: FC<ObjectViewerProps> = ({
outline, outline,
readText, readText,
resolveBlobUrl, resolveBlobUrl,
resolvePrincipal,
highlight, highlight,
onOpenObject, onOpenObject,
actions, actions,
@@ -602,7 +614,7 @@ export const ObjectViewer: FC<ObjectViewerProps> = ({
<OutlineNav sections={outline.outline} activeId={activeSection} onSelect={(s) => setActiveSection(s.id)} /> <OutlineNav sections={outline.outline} activeId={activeSection} onSelect={(s) => setActiveSection(s.id)} />
</div> </div>
)} )}
<ProvenancePanel outline={outline} /> <ProvenancePanel outline={outline} resolvePrincipal={resolvePrincipal} />
</aside> </aside>
</div> </div>
</div> </div>