Add full shell, replaceable Omics adapter and bilingual interaction
Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useI18n } from "../i18n";
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { apiFetch, apiErrorMessage } from "../api/client";
|
||||
@@ -24,38 +25,40 @@ const templates: Record<string, string> = {
|
||||
};
|
||||
|
||||
function CopyText({ text, label, showText = true }: { text: string; label: string; showText?: boolean }) {
|
||||
const { t } = useI18n();
|
||||
const [notice, setNotice] = useState("");
|
||||
return <div className="flex min-w-0 flex-wrap items-start gap-2">
|
||||
{showText && <code className="min-w-0 flex-1 break-all text-xs select-text">{text}</code>}
|
||||
<Button size="sm" variant="outline" aria-label={`Copy ${label}`} onClick={async () => {
|
||||
<Button size="sm" variant="outline" aria-label={t("Copy {label}", { label: label })} onClick={async () => {
|
||||
try { await navigator.clipboard.writeText(text); setNotice("Copied"); }
|
||||
catch { setNotice("Select the text and copy it manually."); }
|
||||
}}>Copy</Button><span role="status" className="text-xs text-muted-foreground">{notice}</span>
|
||||
}}>{t("Copy")}</Button><span role="status" className="text-xs text-muted-foreground">{t(notice)}</span>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export function EvidenceManagementPage({ canManage, initialWorkspaceId }: { canManage: boolean; initialWorkspaceId?: string }) {
|
||||
const { t } = useI18n();
|
||||
const context = useWorkingContextScope();
|
||||
const generation = useAuthGeneration();
|
||||
const [workspace, setWorkspace] = useState(initialWorkspaceId ?? "");
|
||||
const workspaces = useQuery({ queryKey: ["evidence-workspaces", generation],
|
||||
queryFn: () => apiFetch<Array<{ id: string; displayName?: string }>>("/workspaces"), enabled: canManage });
|
||||
if (!canManage) return <main className="p-6">Evidence administration is not permitted.</main>;
|
||||
return <main aria-label="Evidence management" className="thot-administration-page">
|
||||
<AdministrationHeader title="Evidence management" description="Review workspace knowledge and maintain its Markdown files." actions={!context &&
|
||||
<label className="grid min-w-52 gap-1 text-sm">Workspace
|
||||
<select className={control} value={workspace} onChange={event => setWorkspace(event.target.value)}>
|
||||
<option value="">Choose a workspace</option>
|
||||
if (!canManage) return <main className="p-6">{t("Evidence administration is not permitted.")}</main>;
|
||||
return <main aria-label={t("Evidence management")} className="thot-administration-page">
|
||||
<AdministrationHeader title={t("Evidence management")} description={t("Review workspace knowledge and maintain its Markdown files.")} actions={!context &&
|
||||
<label className="grid min-w-52 gap-1 text-sm">{t("Workspace")}<select className={control} value={workspace} onChange={event => setWorkspace(event.target.value)}>
|
||||
<option value="">{t("Choose a workspace")}</option>
|
||||
{workspaces.data?.map(w => <option key={w.id} value={w.id}>{w.displayName ?? w.id}</option>)}
|
||||
</select></label>
|
||||
} />
|
||||
{workspaces.isError && <p role="alert" className="p-6">{apiErrorMessage(workspaces.error)}</p>}
|
||||
{workspaces.isError && <p role="alert" className="p-6">{t(apiErrorMessage(workspaces.error))}</p>}
|
||||
{workspace ? <WorkspaceEvidence key={`${generation}:${workspace}`} workspace={workspace} generation={generation} />
|
||||
: <p className="p-8 text-muted-foreground">Choose a workspace to browse its Evidence and locate the files to edit.</p>}
|
||||
: <p className="p-8 text-muted-foreground">{t("Choose a workspace to browse its Evidence and locate the files to edit.")}</p>}
|
||||
</main>;
|
||||
}
|
||||
|
||||
function WorkspaceEvidence({ workspace, generation }: { workspace: string; generation: number }) {
|
||||
const { t } = useI18n();
|
||||
const [filters, setFilters] = useState<Record<string, string>>({ q: "", kind: "", purpose: "", status: "", sort: "title", direction: "asc" });
|
||||
const [page, setPage] = useState(1);
|
||||
const [selected, setSelected] = useState("");
|
||||
@@ -66,75 +69,76 @@ function WorkspaceEvidence({ workspace, generation }: { workspace: string; gener
|
||||
{list.data && <Instructions data={list.data} />}
|
||||
<EvidenceSources workspace={workspace} sources={list.data?.source_reviews ?? []} />
|
||||
<div className="flex flex-wrap items-end gap-3">
|
||||
<label className="grid flex-1 gap-1 text-sm">Search Evidence<input className={control} value={filters.q} onChange={e => filter("q", e.target.value)} placeholder="Title, identifier or content" /></label>
|
||||
<Select label="Kind" value={filters.kind} values={kinds} onChange={v => filter("kind", v)} />
|
||||
<Select label="Purpose" value={filters.purpose} values={["disambiguation", "rewriting", "schema_linking", "sql_generation"]} onChange={v => filter("purpose", v)} />
|
||||
<Select label="Status" value={filters.status} values={Object.keys(states)} labels={states} onChange={v => filter("status", v)} />
|
||||
<Button variant="outline" disabled={list.isFetching || detail.isFetching} onClick={() => { void list.refetch(); if (selected) void detail.refetch(); }}>Refresh files</Button>
|
||||
<label className="grid flex-1 gap-1 text-sm">{t("Search Evidence")}<input className={control} value={filters.q} onChange={e => filter("q", e.target.value)} placeholder={t("Title, identifier or content")} /></label>
|
||||
<Select label={t("Kind")} value={filters.kind} values={kinds} onChange={v => filter("kind", v)} />
|
||||
<Select label={t("Purpose")} value={filters.purpose} values={["disambiguation", "rewriting", "schema_linking", "sql_generation"]} onChange={v => filter("purpose", v)} />
|
||||
<Select label={t("Status")} value={filters.status} values={Object.keys(states)} labels={states} onChange={v => filter("status", v)} />
|
||||
<Button variant="outline" disabled={list.isFetching || detail.isFetching} onClick={() => { void list.refetch(); if (selected) void detail.refetch(); }}>{t("Refresh files")}</Button>
|
||||
</div>
|
||||
<details><summary className="cursor-pointer text-sm font-medium">More filters and sorting</summary>
|
||||
<div className="mt-3 flex flex-wrap gap-3">{["concept", "table", "column", "source"].map(key => <label key={key} className="grid gap-1 text-sm capitalize">{key}<input className={control} value={filters[key] ?? ""} onChange={e => filter(key, e.target.value)} /></label>)}
|
||||
<Select label="Sort by" value={filters.sort} values={["title", "id", "kind", "status"]} onChange={v => filter("sort", v)} all={false} />
|
||||
<Select label="Order" value={filters.direction} values={["asc", "desc"]} labels={{ asc: "Ascending", desc: "Descending" }} onChange={v => filter("direction", v)} all={false} />
|
||||
<details><summary className="cursor-pointer text-sm font-medium">{t("More filters and sorting")}</summary>
|
||||
<div className="mt-3 flex flex-wrap gap-3">{["concept", "table", "column", "source"].map(key => <label key={key} className="grid gap-1 text-sm capitalize">{t(key)}<input className={control} value={filters[key] ?? ""} onChange={e => filter(key, e.target.value)} /></label>)}
|
||||
<Select label={t("Sort by")} value={filters.sort} values={["title", "id", "kind", "status"]} onChange={v => filter("sort", v)} all={false} />
|
||||
<Select label={t("Order")} value={filters.direction} values={["asc", "desc"]} labels={{ asc: t("Ascending"), desc: t("Descending") }} onChange={v => filter("direction", v)} all={false} />
|
||||
</div></details>
|
||||
{list.isError && <p role="alert">{apiErrorMessage(list.error)}</p>}
|
||||
{list.isPending && <p role="status">Loading Evidence files…</p>}
|
||||
{list.data?.errors.map(error => <div key={error.file} role="alert" className="rounded-md border border-destructive/40 p-3 text-sm"><strong>{error.file}</strong><p>{error.message}</p><p>Correct this file before consolidation.</p></div>)}
|
||||
{list.isError && <p role="alert">{t(apiErrorMessage(list.error))}</p>}
|
||||
{list.isPending && <p role="status">{t("Loading Evidence files…")}</p>}
|
||||
{list.data?.errors.map(error => <div key={error.file} role="alert" className="rounded-md border border-destructive/40 p-3 text-sm"><strong>{error.file}</strong><p>{t(error.message)}</p><p>{t("Correct this file before consolidation.")}</p></div>)}
|
||||
<div className="thot-administration-split" style={{ margin: 0 }}>
|
||||
<section aria-label="Evidence list" className="thot-administration-index p-4">
|
||||
<h2 className="thot-administration-eyebrow mb-3">Evidence library</h2>
|
||||
<p className="mb-2 text-sm text-muted-foreground">{list.data?.total ?? 0} Evidence units</p>
|
||||
<div className="overflow-auto"><table className="w-full text-left text-sm"><thead><tr className="border-b text-muted-foreground"><th className="py-2 pr-3 font-medium">Evidence</th><th className="pr-3 font-medium">Kind</th><th className="font-medium">Status</th></tr></thead>
|
||||
<tbody>{list.data?.items.map(item => <tr key={item.id} className={`border-b ${selected === item.id ? "bg-muted" : ""}`}><td className="py-3 pr-3"><button className="text-left font-medium underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring" onClick={() => setSelected(item.id)}>{item.title}</button><div className="mt-1 break-all text-xs text-muted-foreground">{item.id}</div></td><td className="pr-3 align-top pt-3">{item.kind}</td><td className="align-top pt-3">{states[item.status]}</td></tr>)}</tbody>
|
||||
<section aria-label={t("Evidence list")} className="thot-administration-index p-4">
|
||||
<h2 className="thot-administration-eyebrow mb-3">{t("Evidence library")}</h2>
|
||||
<p className="mb-2 text-sm text-muted-foreground">{list.data?.total ?? 0} {t("Evidence units")}</p>
|
||||
<div className="overflow-auto"><table className="w-full text-left text-sm"><thead><tr className="border-b text-muted-foreground"><th className="py-2 pr-3 font-medium">{t("Evidence")}</th><th className="pr-3 font-medium">{t("Kind")}</th><th className="font-medium">{t("Status")}</th></tr></thead>
|
||||
<tbody>{list.data?.items.map(item => <tr key={item.id} className={`border-b ${selected === item.id ? "bg-muted" : ""}`}><td className="py-3 pr-3"><button className="text-left font-medium underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring" onClick={() => setSelected(item.id)}>{item.title}</button><div className="mt-1 break-all text-xs text-muted-foreground">{item.id}</div></td><td className="pr-3 align-top pt-3">{t(item.kind)}</td><td className="align-top pt-3">{t(states[item.status])}</td></tr>)}</tbody>
|
||||
</table></div>
|
||||
{list.data?.total === 0 && <p className="py-6 text-sm text-muted-foreground">No Evidence matches these filters. Clear the filters or use the file instructions to create a unit.</p>}
|
||||
<nav aria-label="Evidence pages" className="mt-4 flex items-center gap-3"><Button variant="outline" size="sm" disabled={page === 1 || list.isFetching} onClick={() => setPage(p => p-1)}>Previous</Button><span className="text-sm">Page {page}</span><Button variant="outline" size="sm" disabled={!list.data || page*25 >= list.data.total || list.isFetching} onClick={() => setPage(p => p+1)}>Next</Button></nav>
|
||||
{list.data?.total === 0 && <p className="py-6 text-sm text-muted-foreground">{t("No Evidence matches these filters. Clear the filters or use the file instructions to create a unit.")}</p>}
|
||||
<nav aria-label={t("Evidence pages")} className="mt-4 flex items-center gap-3"><Button variant="outline" size="sm" disabled={page === 1 || list.isFetching} onClick={() => setPage(p => p-1)}>{t("Previous")}</Button><span className="text-sm">{t("Page")} {page}</span><Button variant="outline" size="sm" disabled={!list.data || page*25 >= list.data.total || list.isFetching} onClick={() => setPage(p => p+1)}>{t("Next")}</Button></nav>
|
||||
</section>
|
||||
<section aria-label="Evidence detail" className="thot-administration-detail">
|
||||
{detail.isError ? <p role="alert">{apiErrorMessage(detail.error)}</p> : selected && detail.isPending ? <p role="status">Loading Evidence…</p> : detail.data?.item ? <UnitDetail item={detail.data.item} location={detail.data.location} /> : <p className="py-10 text-sm text-muted-foreground">Select an Evidence unit to read its complete content and provenance.</p>}
|
||||
<section aria-label={t("Evidence detail")} className="thot-administration-detail">
|
||||
{detail.isError ? <p role="alert">{t(apiErrorMessage(detail.error))}</p> : selected && detail.isPending ? <p role="status">{t("Loading Evidence…")}</p> : detail.data?.item ? <UnitDetail item={detail.data.item} location={detail.data.location} /> : <p className="py-10 text-sm text-muted-foreground">{t("Select an Evidence unit to read its complete content and provenance.")}</p>}
|
||||
</section>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Select({ label, value, values, labels = {}, onChange, all = true }: { label: string; value: string; values: string[]; labels?: Record<string, string>; onChange: (value: string) => void; all?: boolean }) {
|
||||
return <label className="grid gap-1 text-sm">{label}<select className={control} value={value} onChange={e => onChange(e.target.value)}>{all && <option value="">All</option>}{values.map(v => <option key={v} value={v}>{labels[v] ?? v.replaceAll("_", " ")}</option>)}</select></label>;
|
||||
const { t } = useI18n();
|
||||
return <label className="grid gap-1 text-sm">{t(label)}<select className={control} value={value} onChange={e => onChange(e.target.value)}>{all && <option value="">{t("All")}</option>}{values.map(v => <option key={v} value={v}>{t(labels[v] ?? v.replaceAll("_", " "))}</option>)}</select></label>;
|
||||
}
|
||||
|
||||
function Instructions({ data }: { data: EvidencePage }) {
|
||||
const { t } = useI18n();
|
||||
const [kind, setKind] = useState("domain");
|
||||
const example = `---\nschema_version: 4\nid: evidence:my-unit\nkind: ${kind}\nlanguage: en\npurposes: [sql_generation]\n---\n\n# My Evidence\n\n${templates[kind]}\n`;
|
||||
return <div className="border-b pb-5">
|
||||
<p className="mb-2 text-sm font-medium">{data.pending_revision ? "Consolidation is incomplete. The previous active content remains available." : data.active_revision ? "The core uses the last successfully consolidated version." : "Run the first consolidation to convert and activate the local archive."}</p>
|
||||
{data.location.workspace ? <><p className="mb-1 text-xs text-muted-foreground">Workspace on the installation host</p><CopyText text={data.location.workspace} label="workspace path" /></> : <p role="alert" className="text-sm">A host file path is unavailable for this installation. Configure a persistent workspace-registry bind mount before editing files from the host.</p>}
|
||||
<details className="mt-3"><summary className="cursor-pointer text-sm font-medium">Edit files, consolidate and commit</summary>
|
||||
<p className="mb-2 text-sm font-medium">{data.pending_revision ? t("Consolidation is incomplete. The previous active content remains available.") : data.active_revision ? t("The core uses the last successfully consolidated version.") : t("Run the first consolidation to convert and activate the local archive.")}</p>
|
||||
{data.location.workspace ? <><p className="mb-1 text-xs text-muted-foreground">{t("Workspace on the installation host")}</p><CopyText text={data.location.workspace} label={t("workspace path")} /></> : <p role="alert" className="text-sm">{t("A host file path is unavailable for this installation. Configure a persistent workspace-registry bind mount before editing files from the host.")}</p>}
|
||||
<details className="mt-3"><summary className="cursor-pointer text-sm font-medium">{t("Edit files, consolidate and commit")}</summary>
|
||||
<ol className="mt-3 max-w-[75ch] list-decimal space-y-3 pl-5 text-sm">
|
||||
<li>Edit the files under <code>evidence/curated/<kind>/</code> with your preferred editor. Keep existing IDs. Remove a unit's file to delete it; keep the curated directory.</li>
|
||||
<li>Check Git status and the diff. Saving a file does not update recall.
|
||||
{data.location.git_commands.slice(0, 2).map(command => <div className="mt-2" key={command}><CopyText text={command} label="Git inspection command" /></div>)}</li>
|
||||
<li>From the installation directory, run this command. It validates and activates Evidence without a full preprocessing run. If needed, add <code>--installation /absolute/path/thothii-installation.yaml</code> immediately after <code>tht</code>.
|
||||
<div className="mt-2"><CopyText text={data.location.command} label="consolidation command" /></div></li>
|
||||
<li>After success, review the generated metadata and complete Git commit/push manually. These commands use a POSIX shell on the installation host. A remote host's path is not a file on this browser's computer.
|
||||
{data.location.git_commands.slice(2).map(command => <div className="mt-2" key={command}><CopyText text={command} label="Git publication command" /></div>)}</li>
|
||||
<li>{t("Edit the files under")} <code>evidence/curated/<kind>/</code> {t("with your preferred editor. Keep existing IDs. Remove a unit's file to delete it; keep the curated directory.")}</li>
|
||||
<li>{t("Check Git status and the diff. Saving a file does not update recall.")}{data.location.git_commands.slice(0, 2).map(command => <div className="mt-2" key={command}><CopyText text={command} label={t("Git inspection command")} /></div>)}</li>
|
||||
<li>{t("From the installation directory, run this command. It validates and activates Evidence without a full preprocessing run. If needed, add")} <code>--installation /absolute/path/thothii-installation.yaml</code> {t("immediately after")} <code>tht</code>.
|
||||
<div className="mt-2"><CopyText text={data.location.command} label={t("consolidation command")} /></div></li>
|
||||
<li>{t("After success, review the generated metadata and complete Git commit/push manually. These commands use a POSIX shell on the installation host. A remote host's path is not a file on this browser's computer.")}{data.location.git_commands.slice(2).map(command => <div className="mt-2" key={command}><CopyText text={command} label={t("Git publication command")} /></div>)}</li>
|
||||
</ol>
|
||||
<p className="mt-3 text-sm text-muted-foreground">Source drafts are preserved under evidence/source/. Maintain the refined files under evidence/curated/. New source acquisition and refresh are not part of consolidation.</p>
|
||||
<details className="mt-4"><summary className="cursor-pointer text-sm font-medium">Create a manual Evidence file</summary><div className="mt-3"><Select label="Example kind" value={kind} values={kinds} onChange={setKind} all={false} /><p className="my-2 text-sm">Save as <code>evidence/curated/{kind}/my-unit.md</code> with a unique ID. Replace the example with your domain knowledge. The system records manual provenance.</p><pre className="overflow-auto rounded-md bg-muted p-3 text-xs">{example}</pre><CopyText text={example} label="Markdown example" showText={false} /></div></details>
|
||||
<p className="mt-3 text-sm text-muted-foreground">{t("Source drafts are preserved under evidence/source/. Maintain the refined files under evidence/curated/. New source acquisition and refresh are not part of consolidation.")}</p>
|
||||
<details className="mt-4"><summary className="cursor-pointer text-sm font-medium">{t("Create a manual Evidence file")}</summary><div className="mt-3"><Select label={t("Example kind")} value={kind} values={kinds} onChange={setKind} all={false} /><p className="my-2 text-sm">{t("Save as")} <code>evidence/curated/{kind}/my-unit.md</code> {t("with a unique ID. Replace the example with your domain knowledge. The system records manual provenance.")}</p><pre className="overflow-auto rounded-md bg-muted p-3 text-xs">{example}</pre><CopyText text={example} label={t("Markdown example")} showText={false} /></div></details>
|
||||
</details>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function UnitDetail({ item, location }: { item: EvidenceUnit; location: EvidencePage["location"] }) {
|
||||
const { t } = useI18n();
|
||||
const source = item.provenance.kind === "manual" ? item.provenance.original : item.provenance;
|
||||
const file = location.workspace ? `${location.workspace.replace(/\\/g, "/")}/evidence/${item.file}` : null;
|
||||
return <article className="max-w-[75ch] space-y-5 pb-8">
|
||||
<header><p className="text-xs text-muted-foreground">{states[item.status]} · {item.purposes.join(", ")}</p><h2 className="mt-1 font-serif text-xl font-semibold">{item.title}</h2></header>
|
||||
{file && <CopyText text={file} label="Evidence file path" />}
|
||||
{item.status !== "active" && <p className="text-sm text-muted-foreground">{["invalid", "removed"].includes(item.status) ? "The file is invalid or removed. Its last active content is shown here." : "This is the file version awaiting consolidation."} The core continues to use its last active version.</p>}
|
||||
{Object.entries(item.applies_to).some(([, values]) => values.length > 0) && <section><h3 className="font-medium">Scope</h3>{Object.entries(item.applies_to).map(([name, values]) => values.length > 0 && <p className="mt-2 break-words text-sm" key={name}><span className="capitalize">{name}</span>: {values.join(", ")}</p>)}</section>}
|
||||
{Object.entries(item.payload).map(([field, value]) => <section key={field}><h3 className="mb-2 text-sm font-semibold capitalize">{field.replaceAll("_", " ")}</h3>{typeof value === "string" ? field === "sql" ? <pre className="overflow-auto rounded-md bg-muted p-3 text-sm">{value}</pre> : <MarkdownView source={value} /> : Array.isArray(value) ? <ul className="list-inside list-disc text-sm">{value.map(v => <li key={v}>{v}</li>)}</ul> : <dl className="space-y-2 text-sm">{Object.entries(value).map(([key, text]) => <div key={key}><dt className="font-mono font-medium">{key || "(empty value)"}</dt><dd>{text}</dd></div>)}</dl>}</section>)}
|
||||
<section className="border-t pt-4"><h3 className="font-medium">Provenance</h3><p className="mt-2 text-sm">{item.provenance.kind === "manual" ? `Manual declaration by ${item.provenance.declared_by}.` : "Derived from a source document."}</p>
|
||||
{source?.source_file && <><p className="mt-2 break-all text-xs">{item.provenance.kind === "manual" ? "Original document (lineage): " : "Source: "}{source.source_file}</p>{source.supporting_excerpts?.map((text, i) => <p key={i} className="mt-2 whitespace-pre-wrap text-sm text-muted-foreground">“{text}”</p>)}</>}
|
||||
<header><p className="text-xs text-muted-foreground">{t(states[item.status])} · {item.purposes.map(purpose => t(purpose.replaceAll("_", " "))).join(", ")}</p><h2 className="mt-1 font-serif text-xl font-semibold">{item.title}</h2></header>
|
||||
{file && <CopyText text={file} label={t("Evidence file path")} />}
|
||||
{item.status !== "active" && <p className="text-sm text-muted-foreground">{["invalid", "removed"].includes(item.status) ? t("The file is invalid or removed. Its last active content is shown here.") : t("This is the file version awaiting consolidation.")} {t("The core continues to use its last active version.")}</p>}
|
||||
{Object.entries(item.applies_to).some(([, values]) => values.length > 0) && <section><h3 className="font-medium">{t("Scope")}</h3>{Object.entries(item.applies_to).map(([name, values]) => values.length > 0 && <p className="mt-2 break-words text-sm" key={name}><span className="capitalize">{t(name)}</span>: {values.join(", ")}</p>)}</section>}
|
||||
{Object.entries(item.payload).map(([field, value]) => <section key={field}><h3 className="mb-2 text-sm font-semibold capitalize">{t(field.replaceAll("_", " "))}</h3>{typeof value === "string" ? field === "sql" ? <pre className="overflow-auto rounded-md bg-muted p-3 text-sm">{value}</pre> : <MarkdownView source={value} /> : Array.isArray(value) ? <ul className="list-inside list-disc text-sm">{value.map(v => <li key={v}>{v}</li>)}</ul> : <dl className="space-y-2 text-sm">{Object.entries(value).map(([key, text]) => <div key={key}><dt className="font-mono font-medium">{key || t("(empty value)")}</dt><dd>{text}</dd></div>)}</dl>}</section>)}
|
||||
<section className="border-t pt-4"><h3 className="font-medium">{t("Provenance")}</h3><p className="mt-2 text-sm">{item.provenance.kind === "manual" ? t("Manual declaration by {author}.", { author: item.provenance.declared_by ?? "" }) : t("Derived from a source document.")}</p>
|
||||
{source?.source_file && <><p className="mt-2 break-all text-xs">{item.provenance.kind === "manual" ? t("Original document (lineage): ") : t("Source: ")}{source.source_file}</p>{source.supporting_excerpts?.map((text, i) => <p key={i} className="mt-2 whitespace-pre-wrap text-sm text-muted-foreground">“{text}”</p>)}</>}
|
||||
</section>
|
||||
{item.review_items.length > 0 && <section><h3 className="font-medium">Review required</h3>{item.review_items.map((review, i) => <p key={i} className="mt-2 text-sm">{review.message}</p>)}</section>}
|
||||
{item.review_items.length > 0 && <section><h3 className="font-medium">{t("Review required")}</h3>{item.review_items.map((review, i) => <p key={i} className="mt-2 text-sm">{t(review.message)}</p>)}</section>}
|
||||
</article>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user