Publish documentation / publish (push) Successful in 1m27s
Add PostgreSQL-backed memory, editable evidence with source review and activation, and human-approved archive repairs across the harness, API, and UI. Include migrations, deployment support, regression coverage, and validation documentation. Refresh permissions from validated session roles so existing administrator logins can access newly deployed archive management features.
95 lines
5.8 KiB
TypeScript
95 lines
5.8 KiB
TypeScript
import { useState } from "react";
|
|
import type { WidgetProps } from "./types";
|
|
import { ReservedControls } from "./ReservedControls";
|
|
|
|
interface RepairOption {
|
|
id: string; label: string; archive: "memory" | "evidence"; target_id: string;
|
|
before: Record<string, unknown>; content: Record<string, unknown>;
|
|
}
|
|
interface Repair {
|
|
repair_id: string; reason: string; options: RepairOption[]; choice: string | null;
|
|
status: string; saved: boolean; indexed: boolean; can_apply: boolean;
|
|
}
|
|
const statuses: Record<string, string> = {
|
|
proposed: "Awaiting your decision. No archive changes have been saved.",
|
|
applying: "Your choice is recorded. The file update needs recovery; activation is not confirmed.",
|
|
pending_activation: "Correction saved. Index activation is incomplete; retry to make it available.",
|
|
active: "Correction saved and active in the archive index.",
|
|
rejected: "Proposals rejected. No archive changes were made.",
|
|
superseded: "The archive changed after this correction. Review its current content before proposing another repair.",
|
|
};
|
|
const fieldLabels: Record<string, string> = {
|
|
id: "Identity", subject: "Subject", title: "Title", detail: "Detail", scope: "Scope",
|
|
rationale: "Rationale", question: "Question", sql: "SQL", concepts: "Concepts",
|
|
dependencies: "Dependencies", links: "Links", family: "Family", kind: "Kind",
|
|
language: "Language", purposes: "Purposes", applies_to: "Applies to", payload: "Content",
|
|
provenance: "Source history", review_items: "Review items",
|
|
};
|
|
|
|
function Value({ value }: { value: unknown }) {
|
|
if (value == null) return null;
|
|
if (Array.isArray(value)) return <ul className="space-y-1">{value.map((item, index) =>
|
|
<li key={index}><Value value={item} /></li>)}</ul>;
|
|
if (typeof value === "object") return <dl className="space-y-1">{Object.entries(value).map(([key, item]) =>
|
|
<div key={key}><dt className="text-xs capitalize">{key.replaceAll("_", " ")}</dt>
|
|
<dd className="pl-2"><Value value={item} /></dd></div>)}</dl>;
|
|
return <span>{String(value)}</span>;
|
|
}
|
|
|
|
function Content({ value }: { value: Record<string, unknown> }) {
|
|
return <dl className="min-w-0 space-y-2 text-sm">{Object.entries(fieldLabels).map(([key, label]) => {
|
|
const item = value[key];
|
|
if (item == null || item === "" || (Array.isArray(item) && item.length === 0)) return null;
|
|
return <div key={key}><dt className="font-medium">{label}</dt>
|
|
<dd className="whitespace-pre-wrap break-words text-muted-foreground"><Value value={item} /></dd></div>;
|
|
})}</dl>;
|
|
}
|
|
|
|
export function ArchiveRepairWidget({ descriptor, onRespond }: WidgetProps) {
|
|
const repair = descriptor.repair as Repair;
|
|
const [submitted, setSubmitted] = useState(false);
|
|
const send = (choice: string) => {
|
|
if (submitted) return;
|
|
setSubmitted(true);
|
|
onRespond({ id: descriptor.id, choices: [choice] });
|
|
};
|
|
const pending = repair.status === "applying" || repair.status === "pending_activation";
|
|
return <section aria-label="Archive conflict repair" className="space-y-4 rounded-xl border border-border bg-card p-5">
|
|
<div><h3 className="font-semibold">Resolve archive conflict</h3>
|
|
<p className="mt-1 whitespace-pre-wrap text-sm">{repair.reason}</p></div>
|
|
<p role="status" className="rounded-md bg-muted p-3 text-sm">{statuses[repair.status] ?? repair.status}</p>
|
|
{Boolean(descriptor.error) && <p role="alert" className="text-sm text-destructive">{String(descriptor.error)}</p>}
|
|
{!repair.can_apply && <p className="text-sm text-muted-foreground">An administrator must approve shared archive corrections.
|
|
You can reject the proposals or continue reviewing this question.</p>}
|
|
<fieldset disabled={submitted} className="min-w-0 space-y-4">
|
|
{repair.options.map(option => <article key={option.id} className="space-y-3 border-t border-border pt-4">
|
|
<div><h4 className="text-sm font-semibold">{option.label}</h4>
|
|
<p className="text-xs text-muted-foreground">{option.archive === "memory" ? "Memory" : "Evidence"} · {option.target_id}
|
|
{repair.choice === option.id ? " · Selected" : ""}</p></div>
|
|
<div className="grid min-w-0 gap-4 md:grid-cols-2">
|
|
<div className="min-w-0 rounded-md bg-muted/50 p-3"><p className="mb-2 text-xs font-semibold">Before this proposal</p>
|
|
<Content value={option.before} /></div>
|
|
<div className="min-w-0 rounded-md border border-border p-3"><p className="mb-2 text-xs font-semibold">Proposed result</p>
|
|
<Content value={option.content} /></div>
|
|
</div>
|
|
{repair.can_apply && !repair.choice && <button type="button" onClick={() => send(option.id)}
|
|
className="rounded-md bg-primary px-3 py-2 text-sm font-semibold text-primary-foreground">
|
|
Apply this correction to {option.archive === "memory" ? "Memory" : "Evidence"}
|
|
</button>}
|
|
</article>)}
|
|
<div className="flex flex-wrap gap-3 border-t border-border pt-4">
|
|
{!repair.choice && <button type="button" onClick={() => send("reject")} className="rounded-md border border-border px-3 py-2 text-sm">
|
|
None are adequate — reformulate
|
|
</button>}
|
|
{pending && repair.can_apply && repair.choice && <button type="button" onClick={() => send(repair.choice!)}
|
|
className="rounded-md bg-primary px-3 py-2 text-sm font-semibold text-primary-foreground">Retry selected correction</button>}
|
|
{(repair.choice || !repair.can_apply) && <button type="button" onClick={() => send("continue")}
|
|
className="rounded-md border border-border px-3 py-2 text-sm">{pending ? "Continue with repair pending" : "Continue question review"}</button>}
|
|
</div>
|
|
<ReservedControls reserved={descriptor.reserved} onControl={(control, text) => {
|
|
if (!submitted) { setSubmitted(true); onRespond({ id: descriptor.id, control, text }); }
|
|
}} />
|
|
</fieldset>
|
|
</section>;
|
|
}
|