@kontourai/survey 0.4.13 → 0.4.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/review-workbench/review-presentation.d.ts +67 -0
- package/dist/src/review-workbench/review-presentation.js +133 -0
- package/dist/src/review-workbench/review-surface-preview.d.ts +2 -1
- package/dist/src/review-workbench/review-surface-preview.js +9 -7
- package/dist/src/review-workbench/review-workbench.css +40 -2
- package/dist/src/review-workbench/review-workbench.d.ts +6 -1
- package/dist/src/review-workbench/review-workbench.js +98 -70
- package/dist/src/review-workbench/review-workbench.standalone.css +40 -2
- package/package.json +1 -1
package/dist/src/index.d.ts
CHANGED
|
@@ -19,5 +19,7 @@ export { repeatedObservation } from "./repeated-observation.js";
|
|
|
19
19
|
export type { RepeatedObservationInput } from "./repeated-observation.js";
|
|
20
20
|
export { sourceOfAuthorityObservation, sourceOfAuthorityObservationBuilder, SourceOfAuthorityObservationBuilder, } from "./source-of-authority-observation.js";
|
|
21
21
|
export type { SourceAuthorityClass, SourceAuthorityMetadata, SourceOfAuthorityObservationBuilderArgs, SourceOfAuthorityObservationInput, } from "./source-of-authority-observation.js";
|
|
22
|
+
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-workbench/review-presentation.js";
|
|
23
|
+
export type { ReviewCandidatePresentation, ReviewCandidatePresentationContext, ReviewItemPresentation, ReviewItemPresentationContext, ReviewPresentationAdapter, ReviewPresentationLink, ReviewResultPresentation, ReviewTracePresentationContext, ReviewTraceRef, ReviewValuePresentationContext, } from "./review-workbench/review-presentation.js";
|
|
22
24
|
export { apiRecordSource, manualEntrySource, policyStandardSource, uploadedDocumentSource, webPageSource, } from "./raw-source.js";
|
|
23
25
|
export type { ApiRecordSourceInput, ChecksumInput, ManualEntrySourceInput, PolicyStandardMetadata, PolicyStandardSourceInput, RawSourceInput, UploadedDocumentSourceInput, WebPageSourceInput, } from "./raw-source.js";
|
package/dist/src/index.js
CHANGED
|
@@ -8,4 +8,5 @@ export { buildCanonicalReviewProofPayload, buildReviewProofAnchor, canonicalRevi
|
|
|
8
8
|
export { fieldObservation } from "./field-observation.js";
|
|
9
9
|
export { repeatedObservation } from "./repeated-observation.js";
|
|
10
10
|
export { sourceOfAuthorityObservation, sourceOfAuthorityObservationBuilder, SourceOfAuthorityObservationBuilder, } from "./source-of-authority-observation.js";
|
|
11
|
+
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-workbench/review-presentation.js";
|
|
11
12
|
export { apiRecordSource, manualEntrySource, policyStandardSource, uploadedDocumentSource, webPageSource, } from "./raw-source.js";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type ReviewCandidate, type ReviewItem } from "../review-resource.js";
|
|
2
|
+
import { type ReviewWorkbenchResult } from "./review-workbench.js";
|
|
3
|
+
export interface ReviewPresentationAdapter {
|
|
4
|
+
readonly labelForTarget?: (target: string, context: ReviewItemPresentationContext) => string | undefined;
|
|
5
|
+
readonly labelForCandidateRole?: (role: ReviewCandidate["role"] | undefined, context: ReviewCandidatePresentationContext) => string | undefined;
|
|
6
|
+
readonly summarizeValue?: (value: unknown, context: ReviewValuePresentationContext) => string | undefined;
|
|
7
|
+
readonly linkForReviewItem?: (item: ReviewItem, context: ReviewItemPresentationContext) => ReviewPresentationLink | undefined;
|
|
8
|
+
readonly linkForSource?: (sourceRef: string, context: ReviewCandidatePresentationContext) => ReviewPresentationLink | undefined;
|
|
9
|
+
readonly linkForTraceRef?: (ref: ReviewTraceRef, context: ReviewTracePresentationContext) => ReviewPresentationLink | undefined;
|
|
10
|
+
readonly statusLabel?: (status: string, context: ReviewItemPresentationContext) => string | undefined;
|
|
11
|
+
}
|
|
12
|
+
export interface ReviewItemPresentationContext {
|
|
13
|
+
readonly item: ReviewItem;
|
|
14
|
+
}
|
|
15
|
+
export interface ReviewCandidatePresentationContext extends ReviewItemPresentationContext {
|
|
16
|
+
readonly candidate: ReviewCandidate;
|
|
17
|
+
}
|
|
18
|
+
export interface ReviewValuePresentationContext extends ReviewCandidatePresentationContext {
|
|
19
|
+
readonly value: unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface ReviewTracePresentationContext extends ReviewItemPresentationContext {
|
|
22
|
+
readonly candidate?: ReviewCandidate;
|
|
23
|
+
}
|
|
24
|
+
export interface ReviewPresentationLink {
|
|
25
|
+
readonly label?: string;
|
|
26
|
+
readonly href: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ReviewTraceRef {
|
|
29
|
+
readonly label: string;
|
|
30
|
+
readonly value: string;
|
|
31
|
+
readonly kind: "review-item" | "candidate" | "candidate-set" | "claim" | "source" | "locator" | "proposal" | "external-record";
|
|
32
|
+
readonly link?: ReviewPresentationLink;
|
|
33
|
+
}
|
|
34
|
+
export interface ReviewCandidatePresentation {
|
|
35
|
+
readonly candidate: ReviewCandidate;
|
|
36
|
+
readonly roleLabel: string;
|
|
37
|
+
readonly valueLabel: string;
|
|
38
|
+
readonly valueText: string;
|
|
39
|
+
readonly sourceLabel: string;
|
|
40
|
+
readonly sourceText: string;
|
|
41
|
+
readonly sourceLink?: ReviewPresentationLink;
|
|
42
|
+
readonly traceRefs: readonly ReviewTraceRef[];
|
|
43
|
+
}
|
|
44
|
+
export interface ReviewItemPresentation {
|
|
45
|
+
readonly item: ReviewItem;
|
|
46
|
+
readonly target: string;
|
|
47
|
+
readonly targetLabel: string;
|
|
48
|
+
readonly statusLabel: string;
|
|
49
|
+
readonly reviewItemLink?: ReviewPresentationLink;
|
|
50
|
+
readonly traceRefs: readonly ReviewTraceRef[];
|
|
51
|
+
readonly candidates: readonly ReviewCandidatePresentation[];
|
|
52
|
+
}
|
|
53
|
+
export interface ReviewResultPresentation {
|
|
54
|
+
readonly result: ReviewWorkbenchResult;
|
|
55
|
+
readonly item?: ReviewItem;
|
|
56
|
+
readonly target: string;
|
|
57
|
+
readonly targetLabel: string;
|
|
58
|
+
readonly decisionLabel: string;
|
|
59
|
+
readonly selectedValueText: string;
|
|
60
|
+
readonly applyMeaning: string;
|
|
61
|
+
readonly reviewItemLink?: ReviewPresentationLink;
|
|
62
|
+
readonly traceRefs: readonly ReviewTraceRef[];
|
|
63
|
+
}
|
|
64
|
+
export declare function buildReviewItemPresentation(item: ReviewItem, adapter?: ReviewPresentationAdapter): ReviewItemPresentation;
|
|
65
|
+
export declare function buildReviewCandidatePresentation(item: ReviewItem, candidate: ReviewCandidate, adapter?: ReviewPresentationAdapter, targetLabel?: string): ReviewCandidatePresentation;
|
|
66
|
+
export declare function buildReviewResultPresentation(result: ReviewWorkbenchResult, item: ReviewItem | undefined, adapter?: ReviewPresentationAdapter): ReviewResultPresentation;
|
|
67
|
+
export declare function humanizeIdentifier(value: string): string;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { formatValue } from "./review-surface-preview.js";
|
|
2
|
+
export function buildReviewItemPresentation(item, adapter = {}) {
|
|
3
|
+
const context = { item };
|
|
4
|
+
const targetLabel = adapter.labelForTarget?.(item.spec.target, context) ?? humanizeIdentifier(item.spec.target);
|
|
5
|
+
const status = item.spec.candidateSetStatus ?? "needs-review";
|
|
6
|
+
return {
|
|
7
|
+
item,
|
|
8
|
+
target: item.spec.target,
|
|
9
|
+
targetLabel,
|
|
10
|
+
statusLabel: adapter.statusLabel?.(status, context) ?? humanizeIdentifier(status),
|
|
11
|
+
reviewItemLink: adapter.linkForReviewItem?.(item, context),
|
|
12
|
+
traceRefs: traceRefsForReviewItem(item, adapter),
|
|
13
|
+
candidates: item.spec.candidates.map((candidate) => buildReviewCandidatePresentation(item, candidate, adapter, targetLabel)),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function buildReviewCandidatePresentation(item, candidate, adapter = {}, targetLabel = adapter.labelForTarget?.(item.spec.target, { item }) ?? humanizeIdentifier(item.spec.target)) {
|
|
17
|
+
const context = { item, candidate };
|
|
18
|
+
const sourceRef = candidate.source.sourceRef;
|
|
19
|
+
const sourceLink = adapter.linkForSource?.(sourceRef, context) ?? urlLink(sourceRef);
|
|
20
|
+
return {
|
|
21
|
+
candidate,
|
|
22
|
+
roleLabel: adapter.labelForCandidateRole?.(candidate.role, context) ?? defaultCandidateRoleLabel(candidate.role),
|
|
23
|
+
valueLabel: targetLabel,
|
|
24
|
+
valueText: adapter.summarizeValue?.(candidate.value, { ...context, value: candidate.value }) ?? formatValue(candidate.value),
|
|
25
|
+
sourceLabel: "Source",
|
|
26
|
+
sourceText: sourceLink?.label ?? sourceRef,
|
|
27
|
+
sourceLink,
|
|
28
|
+
traceRefs: traceRefsForCandidate(item, candidate, adapter),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function buildReviewResultPresentation(result, item, adapter = {}) {
|
|
32
|
+
const target = item?.spec.target ?? result.reviewItemName;
|
|
33
|
+
const itemContext = item ? { item } : undefined;
|
|
34
|
+
const targetLabel = item && itemContext
|
|
35
|
+
? adapter.labelForTarget?.(target, itemContext) ?? humanizeIdentifier(target)
|
|
36
|
+
: humanizeIdentifier(target);
|
|
37
|
+
const selectedCandidate = item?.spec.candidates.find((candidate) => candidate.role === result.selectedCandidateRole || candidate.id === result.selectedCandidateId);
|
|
38
|
+
return {
|
|
39
|
+
result,
|
|
40
|
+
item,
|
|
41
|
+
target,
|
|
42
|
+
targetLabel,
|
|
43
|
+
decisionLabel: humanizeIdentifier(result.decision),
|
|
44
|
+
selectedValueText: selectedCandidate && item
|
|
45
|
+
? buildReviewCandidatePresentation(item, selectedCandidate, adapter, targetLabel).valueText
|
|
46
|
+
: result.selectedDisplayValue,
|
|
47
|
+
applyMeaning: result.selectedCandidateRole === "proposed"
|
|
48
|
+
? "Saved decision applies proposed value"
|
|
49
|
+
: "Saved decision keeps current value",
|
|
50
|
+
reviewItemLink: item && itemContext ? adapter.linkForReviewItem?.(item, itemContext) : undefined,
|
|
51
|
+
traceRefs: item
|
|
52
|
+
? traceRefsForResult(item, result, selectedCandidate, adapter)
|
|
53
|
+
: [{ label: "Survey ReviewItem", value: result.reviewItemName, kind: "review-item" }],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function humanizeIdentifier(value) {
|
|
57
|
+
return value
|
|
58
|
+
.replace(/[_-]+/g, " ")
|
|
59
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
60
|
+
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
61
|
+
}
|
|
62
|
+
function traceRefsForReviewItem(item, adapter) {
|
|
63
|
+
const refs = [
|
|
64
|
+
{ label: "Survey ReviewItem", value: item.metadata.name, kind: "review-item" },
|
|
65
|
+
];
|
|
66
|
+
const candidateSetId = item.spec.projection?.candidateSetId;
|
|
67
|
+
if (candidateSetId) {
|
|
68
|
+
refs.push({ label: "Candidate set", value: candidateSetId, kind: "candidate-set" });
|
|
69
|
+
}
|
|
70
|
+
return withTraceLinks(refs, { item }, adapter);
|
|
71
|
+
}
|
|
72
|
+
function traceRefsForCandidate(item, candidate, adapter) {
|
|
73
|
+
const refs = [
|
|
74
|
+
{ label: "Candidate ID", value: candidate.id, kind: "candidate" },
|
|
75
|
+
{
|
|
76
|
+
label: "Claim ID",
|
|
77
|
+
value: candidate.claimTarget.claimId ?? candidate.claimTarget.fieldOrBehavior,
|
|
78
|
+
kind: "claim",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
label: "Source ID",
|
|
82
|
+
value: candidate.source.sourceId ?? candidate.source.sourceRef,
|
|
83
|
+
kind: "source",
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
const locator = candidate.locator?.locator ?? candidate.locator?.scheme;
|
|
87
|
+
if (locator) {
|
|
88
|
+
refs.push({ label: "Locator", value: locator, kind: "locator" });
|
|
89
|
+
}
|
|
90
|
+
return withTraceLinks(refs, { item, candidate }, adapter);
|
|
91
|
+
}
|
|
92
|
+
function traceRefsForResult(item, result, selectedCandidate, adapter) {
|
|
93
|
+
return withTraceLinks([
|
|
94
|
+
{ label: "Survey ReviewItem", value: result.reviewItemName, kind: "review-item" },
|
|
95
|
+
{ label: "Selected candidate", value: result.selectedCandidateId, kind: "candidate" },
|
|
96
|
+
{
|
|
97
|
+
label: "Selected claim",
|
|
98
|
+
value: selectedCandidate?.claimTarget.claimId ?? "not provided",
|
|
99
|
+
kind: "claim",
|
|
100
|
+
},
|
|
101
|
+
], { item, candidate: selectedCandidate }, adapter);
|
|
102
|
+
}
|
|
103
|
+
function withTraceLinks(refs, context, adapter) {
|
|
104
|
+
return refs.map((ref) => ({
|
|
105
|
+
...ref,
|
|
106
|
+
link: ref.link ?? adapter.linkForTraceRef?.(ref, context),
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
function defaultCandidateRoleLabel(role) {
|
|
110
|
+
if (role === "current")
|
|
111
|
+
return "Current value";
|
|
112
|
+
if (role === "proposed")
|
|
113
|
+
return "Proposed value";
|
|
114
|
+
return "Candidate";
|
|
115
|
+
}
|
|
116
|
+
function urlLink(value) {
|
|
117
|
+
if (!/^https?:\/\//.test(value)) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
label: displayUrl(value),
|
|
122
|
+
href: value,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function displayUrl(value) {
|
|
126
|
+
try {
|
|
127
|
+
const parsed = new URL(value);
|
|
128
|
+
return `${parsed.hostname}${parsed.pathname === "/" ? "" : parsed.pathname}`;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReviewDecision, type ReviewItem } from "../review-resource.js";
|
|
2
|
+
import { type ReviewPresentationAdapter } from "./review-presentation.js";
|
|
2
3
|
export interface SurfaceProjectionPreview {
|
|
3
4
|
readonly canonicalClaim: PreviewClaim;
|
|
4
5
|
readonly candidateHistory: PreviewCandidateHistory[];
|
|
@@ -51,5 +52,5 @@ export interface PreviewAuthorityTrace {
|
|
|
51
52
|
readonly label: string;
|
|
52
53
|
readonly detail: string;
|
|
53
54
|
}
|
|
54
|
-
export declare function buildSurfaceProjectionPreview(item: ReviewItem, decision: ReviewDecision | undefined): SurfaceProjectionPreview | undefined;
|
|
55
|
+
export declare function buildSurfaceProjectionPreview(item: ReviewItem, decision: ReviewDecision | undefined, presentationAdapter?: ReviewPresentationAdapter): SurfaceProjectionPreview | undefined;
|
|
55
56
|
export declare function formatValue(value: unknown): string;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
export function buildSurfaceProjectionPreview(item, decision) {
|
|
1
|
+
export function buildSurfaceProjectionPreview(item, decision, presentationAdapter = {}) {
|
|
2
2
|
const selectedCandidate = selectedPreviewCandidate(item, decision);
|
|
3
3
|
if (!selectedCandidate || !decision) {
|
|
4
4
|
return undefined;
|
|
5
5
|
}
|
|
6
6
|
const projection = decision.spec.projection ?? selectedCandidate.projection;
|
|
7
7
|
return {
|
|
8
|
-
canonicalClaim: buildPreviewClaim(selectedCandidate, decision, projection),
|
|
9
|
-
candidateHistory: buildCandidateHistory(item, selectedCandidate),
|
|
8
|
+
canonicalClaim: buildPreviewClaim(item, selectedCandidate, decision, projection, presentationAdapter),
|
|
9
|
+
candidateHistory: buildCandidateHistory(item, selectedCandidate, presentationAdapter),
|
|
10
10
|
sourceEvidence: buildSourceEvidence(selectedCandidate),
|
|
11
11
|
reviewEvent: buildReviewEvent(decision, projection),
|
|
12
12
|
integrityPosture: buildIntegrityPosture(item, selectedCandidate, projection),
|
|
@@ -44,20 +44,22 @@ function selectedPreviewCandidate(item, decision) {
|
|
|
44
44
|
}
|
|
45
45
|
return candidate;
|
|
46
46
|
}
|
|
47
|
-
function buildPreviewClaim(candidate, decision, projection) {
|
|
47
|
+
function buildPreviewClaim(item, candidate, decision, projection, presentationAdapter) {
|
|
48
48
|
return {
|
|
49
49
|
candidateId: candidate.id,
|
|
50
50
|
claimId: projection?.claimId ?? candidate.claimTarget.claimId ?? candidate.claimTarget.fieldOrBehavior,
|
|
51
|
-
value:
|
|
51
|
+
value: presentationAdapter.summarizeValue?.(candidate.value, { item, candidate, value: candidate.value })
|
|
52
|
+
?? formatValue(candidate.value),
|
|
52
53
|
status: decision.spec.status,
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
|
-
function buildCandidateHistory(item, selectedCandidate) {
|
|
56
|
+
function buildCandidateHistory(item, selectedCandidate, presentationAdapter) {
|
|
56
57
|
return item.spec.candidates
|
|
57
58
|
.filter((candidate) => candidate.id !== selectedCandidate.id)
|
|
58
59
|
.map((candidate) => ({
|
|
59
60
|
candidateId: candidate.id,
|
|
60
|
-
value:
|
|
61
|
+
value: presentationAdapter.summarizeValue?.(candidate.value, { item, candidate, value: candidate.value })
|
|
62
|
+
?? formatValue(candidate.value),
|
|
61
63
|
historyLabel: "Unselected candidate history",
|
|
62
64
|
}));
|
|
63
65
|
}
|
|
@@ -771,7 +771,7 @@
|
|
|
771
771
|
|
|
772
772
|
.survey-workbench-embed .candidate-card{
|
|
773
773
|
display: grid;
|
|
774
|
-
grid-template-rows: auto auto minmax(0, 1fr);
|
|
774
|
+
grid-template-rows: auto auto minmax(0, 1fr) auto;
|
|
775
775
|
gap: 11px;
|
|
776
776
|
min-height: auto;
|
|
777
777
|
padding: 14px;
|
|
@@ -817,7 +817,7 @@
|
|
|
817
817
|
color: var(--paper-faint);
|
|
818
818
|
font-size: 10px;
|
|
819
819
|
letter-spacing: 0.1em;
|
|
820
|
-
text-transform:
|
|
820
|
+
text-transform: uppercase;
|
|
821
821
|
overflow-wrap: anywhere;
|
|
822
822
|
word-break: break-word;
|
|
823
823
|
}
|
|
@@ -1110,6 +1110,44 @@
|
|
|
1110
1110
|
color: var(--paper-faint);
|
|
1111
1111
|
}
|
|
1112
1112
|
|
|
1113
|
+
.survey-workbench-embed .reference-details{
|
|
1114
|
+
margin-top: 10px;
|
|
1115
|
+
border-top: 1px solid var(--line);
|
|
1116
|
+
padding-top: 8px;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
.survey-workbench-embed .reference-details summary{
|
|
1120
|
+
cursor: pointer;
|
|
1121
|
+
color: var(--paper-faint);
|
|
1122
|
+
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
|
1123
|
+
font-size: 10px;
|
|
1124
|
+
font-weight: 600;
|
|
1125
|
+
letter-spacing: 0.08em;
|
|
1126
|
+
text-transform: uppercase;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
.survey-workbench-embed .reference-details summary::-webkit-details-marker{
|
|
1130
|
+
display: none;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
.survey-workbench-embed .reference-details summary::before{
|
|
1134
|
+
content: "› ";
|
|
1135
|
+
color: var(--paper-faint);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
.survey-workbench-embed .reference-details[open] summary{
|
|
1139
|
+
color: var(--accent);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
.survey-workbench-embed .reference-details[open] summary::before{
|
|
1143
|
+
content: "⌄ ";
|
|
1144
|
+
color: var(--accent);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
.survey-workbench-embed .reference-details .field-stack{
|
|
1148
|
+
margin-top: 8px;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1113
1151
|
/* ---- Decision payload: terminal readout, intentionally demoted -------- */
|
|
1114
1152
|
|
|
1115
1153
|
.survey-workbench-embed .payload-panel{
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type ReviewQueueSessionState, type ReviewWorkbenchDecision, type ReviewWorkbenchState } from "./review-queue-session.js";
|
|
2
|
+
import { type ReviewPresentationAdapter } from "./review-presentation.js";
|
|
2
3
|
import { type ReviewCandidate, type ReviewDecision, type ReviewSession, type ReviewSessionEvent } from "../review-resource.js";
|
|
3
4
|
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, type ReviewQueueRowStatus, type ReviewQueueSessionState, type ReviewSessionSummary, type ReviewWorkbenchDecision, type ReviewWorkbenchState, } from "./review-queue-session.js";
|
|
5
|
+
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, type ReviewCandidatePresentation, type ReviewCandidatePresentationContext, type ReviewItemPresentation, type ReviewItemPresentationContext, type ReviewPresentationAdapter, type ReviewPresentationLink, type ReviewResultPresentation, type ReviewTracePresentationContext, type ReviewTraceRef, type ReviewValuePresentationContext, } from "./review-presentation.js";
|
|
4
6
|
export { buildSurfaceProjectionPreview, type PreviewAuthorityTrace, type PreviewCandidateHistory, type PreviewClaim, type PreviewIntegrityPosture, type PreviewReviewEvent, type PreviewSourceAuthority, type PreviewSourceEvidence, type SurfaceProjectionPreview, } from "./review-surface-preview.js";
|
|
5
7
|
export declare function buildReviewDecision(state: ReviewWorkbenchState): ReviewDecision | undefined;
|
|
6
8
|
export interface ReviewWorkbenchSessionExport {
|
|
@@ -43,6 +45,7 @@ export interface PersistentReviewSessionEventStoreOptions {
|
|
|
43
45
|
}
|
|
44
46
|
export interface MountReviewWorkbenchOptions {
|
|
45
47
|
readonly eventStore?: ReviewSessionEventStore;
|
|
48
|
+
readonly presentationAdapter?: ReviewPresentationAdapter;
|
|
46
49
|
}
|
|
47
50
|
export interface BrowserReviewWorkbenchConfig {
|
|
48
51
|
readonly startState?: ReviewQueueSessionState | ReviewWorkbenchState;
|
|
@@ -73,7 +76,9 @@ export declare function createLocalStorageReviewSessionEventStore(storage: Pick<
|
|
|
73
76
|
export declare function createPersistentReviewSessionEventStore(options: PersistentReviewSessionEventStoreOptions): ReviewSessionEventStore & {
|
|
74
77
|
events(): readonly ReviewSessionEvent[];
|
|
75
78
|
};
|
|
76
|
-
export declare function renderReviewWorkbenchHtml(state: ReviewWorkbenchState | ReviewQueueSessionState, events?: readonly ReviewSessionEvent[]
|
|
79
|
+
export declare function renderReviewWorkbenchHtml(state: ReviewWorkbenchState | ReviewQueueSessionState, events?: readonly ReviewSessionEvent[], options?: {
|
|
80
|
+
readonly presentationAdapter?: ReviewPresentationAdapter;
|
|
81
|
+
}): string;
|
|
77
82
|
export declare function mountReviewWorkbench(root: HTMLElement, startState?: ReviewQueueSessionState | ReviewWorkbenchState, options?: MountReviewWorkbenchOptions): void;
|
|
78
83
|
export declare function browserReviewWorkbenchStartState(): ReviewQueueSessionState | ReviewWorkbenchState | undefined;
|
|
79
84
|
declare global {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { candidateForDecision, buildReviewSessionEvent, buildReviewSessionEvents, buildReviewSessionResource, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
2
2
|
import { buildSurfaceProjectionPreview, formatValue, } from "./review-surface-preview.js";
|
|
3
|
+
import { buildReviewCandidatePresentation, buildReviewItemPresentation, } from "./review-presentation.js";
|
|
3
4
|
import { reviewResourceApiVersion, } from "../review-resource.js";
|
|
4
5
|
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
6
|
+
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-presentation.js";
|
|
5
7
|
export { buildSurfaceProjectionPreview, } from "./review-surface-preview.js";
|
|
6
8
|
export function buildReviewDecision(state) {
|
|
7
9
|
if (!state.decision) {
|
|
@@ -212,38 +214,38 @@ export function createPersistentReviewSessionEventStore(options) {
|
|
|
212
214
|
},
|
|
213
215
|
};
|
|
214
216
|
}
|
|
215
|
-
export function renderReviewWorkbenchHtml(state, events) {
|
|
217
|
+
export function renderReviewWorkbenchHtml(state, events, options = {}) {
|
|
216
218
|
if ("items" in state) {
|
|
217
|
-
return renderReviewQueueSessionHtml(state, events);
|
|
219
|
+
return renderReviewQueueSessionHtml(state, events, options.presentationAdapter);
|
|
218
220
|
}
|
|
219
221
|
return `
|
|
220
222
|
<section class="workbench-shell" aria-label="Survey review workbench">
|
|
221
|
-
${renderWorkbenchHeader(state)}
|
|
223
|
+
${renderWorkbenchHeader(state, options.presentationAdapter)}
|
|
222
224
|
<div class="content-grid">
|
|
223
|
-
${renderReviewFocus(state)}
|
|
224
|
-
${renderDecisionColumn(state)}
|
|
225
|
-
${renderCandidateComparison(state)}
|
|
225
|
+
${renderReviewFocus(state, options.presentationAdapter)}
|
|
226
|
+
${renderDecisionColumn(state, options.presentationAdapter)}
|
|
227
|
+
${renderCandidateComparison(state, options.presentationAdapter)}
|
|
226
228
|
</div>
|
|
227
229
|
</section>
|
|
228
230
|
`;
|
|
229
231
|
}
|
|
230
|
-
function renderReviewQueueSessionHtml(session, events) {
|
|
232
|
+
function renderReviewQueueSessionHtml(session, events, presentationAdapter) {
|
|
231
233
|
const state = currentReviewWorkbenchState(session);
|
|
232
234
|
const sessionExport = buildReviewWorkbenchSessionExport(session, events);
|
|
233
235
|
return `
|
|
234
236
|
<section class="workbench-shell" aria-label="Survey review workbench">
|
|
235
|
-
${renderWorkbenchHeader(state)}
|
|
236
|
-
${renderActiveReviewStrip(session, state)}
|
|
237
|
+
${renderWorkbenchHeader(state, presentationAdapter)}
|
|
238
|
+
${renderActiveReviewStrip(session, state, presentationAdapter)}
|
|
237
239
|
<div class="queue-layout">
|
|
238
240
|
<aside class="queue-panel" aria-label="Review queue">
|
|
239
|
-
${renderQueueRows(session)}
|
|
241
|
+
${renderQueueRows(session, presentationAdapter)}
|
|
240
242
|
${renderSessionSummary(session)}
|
|
241
243
|
${renderSessionAudit(session, sessionExport)}
|
|
242
244
|
</aside>
|
|
243
245
|
<div class="content-grid">
|
|
244
|
-
${renderReviewFocus(state)}
|
|
245
|
-
${renderDecisionColumn(state)}
|
|
246
|
-
${renderCandidateComparison(state)}
|
|
246
|
+
${renderReviewFocus(state, presentationAdapter)}
|
|
247
|
+
${renderDecisionColumn(state, presentationAdapter)}
|
|
248
|
+
${renderCandidateComparison(state, presentationAdapter)}
|
|
247
249
|
</div>
|
|
248
250
|
</div>
|
|
249
251
|
</section>
|
|
@@ -309,25 +311,26 @@ function renderSessionEventRow(event) {
|
|
|
309
311
|
</li>
|
|
310
312
|
`;
|
|
311
313
|
}
|
|
312
|
-
function renderQueueRows(session) {
|
|
314
|
+
function renderQueueRows(session, presentationAdapter) {
|
|
313
315
|
return `
|
|
314
316
|
<section class="queue-list" data-testid="review-queue">
|
|
315
317
|
<div class="queue-head">
|
|
316
318
|
<h2>Review queue</h2>
|
|
317
319
|
<button class="next-button" type="button" data-testid="next-unresolved">Next unresolved</button>
|
|
318
320
|
</div>
|
|
319
|
-
${session.items.map((item) => renderQueueRow(item, session)).join("")}
|
|
321
|
+
${session.items.map((item) => renderQueueRow(item, session, presentationAdapter)).join("")}
|
|
320
322
|
</section>
|
|
321
323
|
`;
|
|
322
324
|
}
|
|
323
|
-
function renderQueueRow(item, session) {
|
|
325
|
+
function renderQueueRow(item, session, presentationAdapter) {
|
|
324
326
|
const status = deriveQueueRowStatus(item, session);
|
|
325
327
|
const isActive = item.metadata.name === session.activeItemName;
|
|
328
|
+
const presentation = buildReviewItemPresentation(item, presentationAdapter);
|
|
326
329
|
return `
|
|
327
330
|
<button class="queue-row${isActive ? " is-active" : ""}" type="button" data-item-name="${escapeHtml(item.metadata.name)}" data-testid="queue-row" data-queue-status="${status}">
|
|
328
331
|
<span class="queue-row-main">
|
|
329
|
-
<span class="queue-row-title">${escapeHtml(
|
|
330
|
-
<span class="queue-row-target">${escapeHtml(item.
|
|
332
|
+
<span class="queue-row-title">${escapeHtml(presentation.targetLabel)}</span>
|
|
333
|
+
<span class="queue-row-target">${escapeHtml(item.metadata.name)}</span>
|
|
331
334
|
</span>
|
|
332
335
|
<span class="state-label">${escapeHtml(status)}</span>
|
|
333
336
|
</button>
|
|
@@ -348,16 +351,17 @@ function renderSessionSummary(session) {
|
|
|
348
351
|
</section>
|
|
349
352
|
`;
|
|
350
353
|
}
|
|
351
|
-
function renderActiveReviewStrip(session, state) {
|
|
354
|
+
function renderActiveReviewStrip(session, state, presentationAdapter) {
|
|
352
355
|
const activeIndex = session.items.findIndex((item) => item.metadata.name === session.activeItemName);
|
|
353
356
|
const position = activeIndex >= 0 ? activeIndex + 1 : 1;
|
|
354
357
|
const status = deriveQueueRowStatus(state.item, session);
|
|
358
|
+
const presentation = buildReviewItemPresentation(state.item, presentationAdapter);
|
|
355
359
|
return `
|
|
356
360
|
<section class="active-review-strip" data-testid="active-review-strip" aria-label="Active review item">
|
|
357
361
|
<div class="active-review-copy">
|
|
358
362
|
<span class="field-label">Review ${position} of ${session.items.length}</span>
|
|
359
|
-
<strong>${escapeHtml(
|
|
360
|
-
<span>${escapeHtml(state.item.
|
|
363
|
+
<strong>${escapeHtml(presentation.targetLabel)}</strong>
|
|
364
|
+
<span>${escapeHtml(state.item.metadata.name)}</span>
|
|
361
365
|
</div>
|
|
362
366
|
<div class="active-review-actions">
|
|
363
367
|
<span class="state-label">${escapeHtml(status)}</span>
|
|
@@ -366,19 +370,22 @@ function renderActiveReviewStrip(session, state) {
|
|
|
366
370
|
</section>
|
|
367
371
|
`;
|
|
368
372
|
}
|
|
369
|
-
function renderWorkbenchHeader(state) {
|
|
373
|
+
function renderWorkbenchHeader(state, presentationAdapter) {
|
|
370
374
|
const current = candidateByRole(state.item, "current");
|
|
371
375
|
const proposed = candidateByRole(state.item, "proposed");
|
|
376
|
+
const presentation = buildReviewItemPresentation(state.item, presentationAdapter);
|
|
377
|
+
const currentPresentation = buildReviewCandidatePresentation(state.item, current, presentationAdapter, presentation.targetLabel);
|
|
378
|
+
const proposedPresentation = buildReviewCandidatePresentation(state.item, proposed, presentationAdapter, presentation.targetLabel);
|
|
372
379
|
return `
|
|
373
380
|
<header class="topbar">
|
|
374
381
|
<div>
|
|
375
382
|
<p class="eyebrow">${escapeHtml(String(state.item.metadata.producer?.displayName ?? "Survey"))}</p>
|
|
376
383
|
<h1>Review candidate update</h1>
|
|
377
|
-
<p class="review-question">For <strong>${escapeHtml(
|
|
384
|
+
<p class="review-question">For <strong>${escapeHtml(presentation.targetLabel)}</strong>, decide whether <strong>${escapeHtml(proposedPresentation.valueText)}</strong> should replace <strong>${escapeHtml(currentPresentation.valueText)}</strong>.</p>
|
|
378
385
|
</div>
|
|
379
386
|
<dl class="meta-grid">
|
|
380
387
|
${metaItem("Item", state.item.metadata.name)}
|
|
381
|
-
${metaItem("Status",
|
|
388
|
+
${metaItem("Status", presentation.statusLabel)}
|
|
382
389
|
${metaItem("Selected", state.item.spec.selectedCandidateId ?? "none")}
|
|
383
390
|
${metaItem("Candidate count", String(state.item.status?.observedCandidateCount ?? state.item.spec.candidates.length))}
|
|
384
391
|
</dl>
|
|
@@ -393,53 +400,55 @@ function renderReviewMain(state) {
|
|
|
393
400
|
</div>
|
|
394
401
|
`;
|
|
395
402
|
}
|
|
396
|
-
function renderReviewFocus(state) {
|
|
403
|
+
function renderReviewFocus(state, presentationAdapter) {
|
|
397
404
|
const current = candidateByRole(state.item, "current");
|
|
398
405
|
const proposed = candidateByRole(state.item, "proposed");
|
|
399
406
|
const proposedConfidence = proposed.extraction.confidence ?? proposed.confidence;
|
|
400
407
|
const currentConfidence = current.extraction.confidence ?? current.confidence;
|
|
408
|
+
const presentation = buildReviewItemPresentation(state.item, presentationAdapter);
|
|
401
409
|
return `
|
|
402
410
|
<section class="review-focus" data-testid="review-focus" aria-label="Active review focus">
|
|
403
411
|
<div class="focus-head">
|
|
404
412
|
<span class="field-label">Active review</span>
|
|
405
|
-
<span class="state-label">${escapeHtml(
|
|
413
|
+
<span class="state-label">${escapeHtml(presentation.statusLabel)}</span>
|
|
406
414
|
</div>
|
|
407
415
|
<div class="focus-values">
|
|
408
|
-
${focusValue(
|
|
409
|
-
${focusValue(
|
|
416
|
+
${focusValue(state.item, current, currentConfidence, "current", presentationAdapter, presentation.targetLabel)}
|
|
417
|
+
${focusValue(state.item, proposed, proposedConfidence, "proposed", presentationAdapter, presentation.targetLabel)}
|
|
410
418
|
</div>
|
|
411
419
|
<dl class="focus-evidence">
|
|
412
|
-
${fieldItem("Target",
|
|
420
|
+
${fieldItem("Target", presentation.targetLabel)}
|
|
413
421
|
${fieldItem("Proposed source", proposed.source.sourceRef)}
|
|
414
422
|
${fieldItem("Proposed excerpt", proposed.locator?.excerpt ?? "none", "excerpt")}
|
|
415
423
|
</dl>
|
|
416
424
|
</section>
|
|
417
425
|
`;
|
|
418
426
|
}
|
|
419
|
-
function focusValue(
|
|
427
|
+
function focusValue(item, candidate, confidence, tone, presentationAdapter, targetLabel) {
|
|
428
|
+
const presentation = buildReviewCandidatePresentation(item, candidate, presentationAdapter, targetLabel);
|
|
420
429
|
return `
|
|
421
430
|
<div class="focus-value is-${tone}">
|
|
422
|
-
<span class="field-label">${escapeHtml(
|
|
423
|
-
<strong>${escapeHtml(
|
|
431
|
+
<span class="field-label">${escapeHtml(presentation.roleLabel)}</span>
|
|
432
|
+
<strong>${escapeHtml(presentation.valueText)}</strong>
|
|
424
433
|
<span>${escapeHtml(confidence === undefined ? "confidence unknown" : formatConfidence(confidence))}</span>
|
|
425
434
|
</div>
|
|
426
435
|
`;
|
|
427
436
|
}
|
|
428
|
-
function renderCandidateComparison(state) {
|
|
437
|
+
function renderCandidateComparison(state, presentationAdapter) {
|
|
429
438
|
const current = candidateByRole(state.item, "current");
|
|
430
439
|
const proposed = candidateByRole(state.item, "proposed");
|
|
431
440
|
return `
|
|
432
441
|
<section class="candidate-grid" aria-label="Candidate comparison">
|
|
433
|
-
${renderCandidateCard(current, state)}
|
|
434
|
-
${renderCandidateCard(proposed, state)}
|
|
442
|
+
${renderCandidateCard(current, state, presentationAdapter)}
|
|
443
|
+
${renderCandidateCard(proposed, state, presentationAdapter)}
|
|
435
444
|
</section>
|
|
436
445
|
`;
|
|
437
446
|
}
|
|
438
|
-
function renderDecisionColumn(state) {
|
|
447
|
+
function renderDecisionColumn(state, presentationAdapter) {
|
|
439
448
|
return `
|
|
440
449
|
<aside class="decision-column" aria-label="Review decision">
|
|
441
450
|
${renderDecisionControls(state)}
|
|
442
|
-
${renderSurfacePreview(state)}
|
|
451
|
+
${renderSurfacePreview(state, presentationAdapter)}
|
|
443
452
|
${renderDecisionPayload(state)}
|
|
444
453
|
</aside>
|
|
445
454
|
`;
|
|
@@ -490,8 +499,8 @@ function renderDecisionPayload(state) {
|
|
|
490
499
|
</details>
|
|
491
500
|
`;
|
|
492
501
|
}
|
|
493
|
-
function renderSurfacePreview(state) {
|
|
494
|
-
const preview = buildSurfaceProjectionPreview(state.item, buildReviewDecision(state));
|
|
502
|
+
function renderSurfacePreview(state, presentationAdapter) {
|
|
503
|
+
const preview = buildSurfaceProjectionPreview(state.item, buildReviewDecision(state), presentationAdapter);
|
|
495
504
|
return preview ? renderPopulatedSurfacePreview(preview) : renderPendingSurfacePreview();
|
|
496
505
|
}
|
|
497
506
|
function renderPendingSurfacePreview() {
|
|
@@ -530,11 +539,12 @@ function renderSurfacePreviewSections(preview) {
|
|
|
530
539
|
].join("");
|
|
531
540
|
}
|
|
532
541
|
function renderCanonicalClaim(preview) {
|
|
533
|
-
return renderPreviewSection("Selected
|
|
534
|
-
["Candidate", preview.canonicalClaim.candidateId],
|
|
535
|
-
["Claim", preview.canonicalClaim.claimId],
|
|
542
|
+
return renderPreviewSection("Selected claim", "surface-canonical-claim", [
|
|
536
543
|
["Value", preview.canonicalClaim.value],
|
|
537
544
|
["Review status", preview.canonicalClaim.status],
|
|
545
|
+
], undefined, [
|
|
546
|
+
["Candidate ID", preview.canonicalClaim.candidateId],
|
|
547
|
+
["Claim ID", preview.canonicalClaim.claimId],
|
|
538
548
|
]);
|
|
539
549
|
}
|
|
540
550
|
function renderReviewEvent(preview) {
|
|
@@ -548,10 +558,11 @@ function renderReviewEvent(preview) {
|
|
|
548
558
|
}
|
|
549
559
|
function renderIntegrityPosture(preview) {
|
|
550
560
|
return renderPreviewSection("Integrity posture", "surface-integrity-posture", [
|
|
551
|
-
["Candidate set", preview.integrityPosture.candidateSetId],
|
|
552
|
-
["Raw source", preview.integrityPosture.rawSourceId],
|
|
553
|
-
["Extraction", preview.integrityPosture.extractionId],
|
|
554
561
|
["Checksum", preview.integrityPosture.checksum],
|
|
562
|
+
], undefined, [
|
|
563
|
+
["Candidate set ID", preview.integrityPosture.candidateSetId],
|
|
564
|
+
["Raw source ID", preview.integrityPosture.rawSourceId],
|
|
565
|
+
["Extraction ID", preview.integrityPosture.extractionId],
|
|
555
566
|
]);
|
|
556
567
|
}
|
|
557
568
|
function renderAuthorityTrace(preview) {
|
|
@@ -565,35 +576,50 @@ function renderCandidateHistory(preview) {
|
|
|
565
576
|
? [["History", "No unselected candidates."]]
|
|
566
577
|
: preview.candidateHistory.flatMap((candidate) => [
|
|
567
578
|
["History", candidate.historyLabel],
|
|
568
|
-
["Candidate", candidate.candidateId],
|
|
569
579
|
["Value", candidate.value],
|
|
570
580
|
]);
|
|
571
|
-
|
|
581
|
+
const references = preview.candidateHistory.flatMap((candidate) => [
|
|
582
|
+
["Candidate ID", candidate.candidateId],
|
|
583
|
+
]);
|
|
584
|
+
return renderPreviewSection("Unselected candidate history", "surface-candidate-history", rows, undefined, references);
|
|
572
585
|
}
|
|
573
586
|
function renderSourceEvidence(preview) {
|
|
574
587
|
const rows = [
|
|
575
588
|
["Source URL", preview.sourceEvidence.sourceRef],
|
|
576
|
-
["Source ref", preview.sourceEvidence.sourceId],
|
|
577
589
|
["Excerpt", preview.sourceEvidence.excerpt],
|
|
578
|
-
["Extraction", preview.sourceEvidence.extractionId],
|
|
579
590
|
["Extractor", preview.sourceEvidence.extractor],
|
|
580
591
|
["Observed", preview.sourceEvidence.observedAt],
|
|
581
592
|
];
|
|
593
|
+
const references = [
|
|
594
|
+
["Source ID", preview.sourceEvidence.sourceId],
|
|
595
|
+
["Extraction ID", preview.sourceEvidence.extractionId],
|
|
596
|
+
];
|
|
582
597
|
if (preview.sourceEvidence.sourceAuthority) {
|
|
583
598
|
rows.push(["Source authority class", preview.sourceEvidence.sourceAuthority.authorityClass], ["Declared by", preview.sourceEvidence.sourceAuthority.declaredBy], ["Authority scope", preview.sourceEvidence.sourceAuthority.scope]);
|
|
584
599
|
}
|
|
585
600
|
return renderPreviewSection("Source evidence", "surface-source-evidence", rows);
|
|
586
601
|
}
|
|
587
|
-
function renderPreviewSection(title, testId, rows, extraClass = "") {
|
|
602
|
+
function renderPreviewSection(title, testId, rows, extraClass = "", references = []) {
|
|
588
603
|
return `
|
|
589
604
|
<section class="preview-section${extraClass}" data-testid="${testId}">
|
|
590
605
|
<h3>${escapeHtml(title)}</h3>
|
|
591
606
|
<dl class="field-stack compact">
|
|
592
607
|
${rows.map(([label, value]) => fieldItem(label, value)).join("")}
|
|
593
608
|
</dl>
|
|
609
|
+
${references.length > 0 ? renderReferenceDetails(references) : ""}
|
|
594
610
|
</section>
|
|
595
611
|
`;
|
|
596
612
|
}
|
|
613
|
+
function renderReferenceDetails(references) {
|
|
614
|
+
return `
|
|
615
|
+
<details class="reference-details">
|
|
616
|
+
<summary>IDs and trace links</summary>
|
|
617
|
+
<dl class="field-stack compact">
|
|
618
|
+
${references.map(([label, value]) => fieldItem(label, value)).join("")}
|
|
619
|
+
</dl>
|
|
620
|
+
</details>
|
|
621
|
+
`;
|
|
622
|
+
}
|
|
597
623
|
export function mountReviewWorkbench(root, startState = initialReviewQueueSessionState(), options = {}) {
|
|
598
624
|
const controller = createReviewWorkbenchController(root, startState, options);
|
|
599
625
|
controller.renderCurrentState();
|
|
@@ -651,7 +677,7 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
651
677
|
appendEvent("item-selected");
|
|
652
678
|
persistEvents();
|
|
653
679
|
},
|
|
654
|
-
renderCurrentState: () => renderCurrentState(root, session, controller),
|
|
680
|
+
renderCurrentState: () => renderCurrentState(root, session, controller, options.presentationAdapter),
|
|
655
681
|
selectDecision,
|
|
656
682
|
selectQueueItem: (itemName) => {
|
|
657
683
|
applySessionUpdate((current) => ({ ...current, activeItemName: itemName }));
|
|
@@ -692,9 +718,9 @@ function reviewSessionStorageKey(session, keyPrefix) {
|
|
|
692
718
|
const itemNames = buildReviewSessionResource(session).spec.reviewItemNames.join(",");
|
|
693
719
|
return `${keyPrefix}:${itemNames}`;
|
|
694
720
|
}
|
|
695
|
-
function renderCurrentState(root, session, controller) {
|
|
696
|
-
root.innerHTML = renderReviewWorkbenchHtml(session, controller.currentSessionExport().events);
|
|
697
|
-
bindReviewerNote(root, controller.updateReviewerNote, controller.currentState, controller.currentSession, controller.currentSessionExport, controller.renderCurrentState);
|
|
721
|
+
function renderCurrentState(root, session, controller, presentationAdapter) {
|
|
722
|
+
root.innerHTML = renderReviewWorkbenchHtml(session, controller.currentSessionExport().events, { presentationAdapter });
|
|
723
|
+
bindReviewerNote(root, controller.updateReviewerNote, controller.currentState, controller.currentSession, controller.currentSessionExport, controller.renderCurrentState, presentationAdapter);
|
|
698
724
|
bindDecisionButtons(root, (decision) => {
|
|
699
725
|
controller.selectDecision(decision);
|
|
700
726
|
controller.renderCurrentState();
|
|
@@ -721,10 +747,10 @@ function queueSessionFromStartState(startState) {
|
|
|
721
747
|
actorId: startState.actorId,
|
|
722
748
|
};
|
|
723
749
|
}
|
|
724
|
-
function bindReviewerNote(root, updateReviewerNote, currentState, currentSession, currentSessionExport, renderCurrentState) {
|
|
750
|
+
function bindReviewerNote(root, updateReviewerNote, currentState, currentSession, currentSessionExport, renderCurrentState, presentationAdapter) {
|
|
725
751
|
root.querySelector("[data-testid='reviewer-note']")?.addEventListener("input", (event) => {
|
|
726
752
|
updateReviewerNote(event.target.value);
|
|
727
|
-
refreshDecisionOutputs(root, currentState(), renderCurrentState);
|
|
753
|
+
refreshDecisionOutputs(root, currentState(), renderCurrentState, presentationAdapter);
|
|
728
754
|
refreshSessionAudit(root, currentSession(), currentSessionExport(), renderCurrentState);
|
|
729
755
|
});
|
|
730
756
|
}
|
|
@@ -762,7 +788,7 @@ function scrollActiveReviewIntoView(root) {
|
|
|
762
788
|
behavior: "smooth",
|
|
763
789
|
});
|
|
764
790
|
}
|
|
765
|
-
function refreshDecisionOutputs(root, state, renderCurrentState) {
|
|
791
|
+
function refreshDecisionOutputs(root, state, renderCurrentState, presentationAdapter) {
|
|
766
792
|
const payload = root.querySelector("[data-testid='decision-payload']");
|
|
767
793
|
if (payload) {
|
|
768
794
|
payload.textContent = JSON.stringify(buildReviewDecision(state) ?? null, null, 2);
|
|
@@ -776,7 +802,7 @@ function refreshDecisionOutputs(root, state, renderCurrentState) {
|
|
|
776
802
|
return;
|
|
777
803
|
}
|
|
778
804
|
const wrapper = document.createElement("div");
|
|
779
|
-
wrapper.innerHTML = renderSurfacePreview(state);
|
|
805
|
+
wrapper.innerHTML = renderSurfacePreview(state, presentationAdapter);
|
|
780
806
|
preview.replaceWith(wrapper.firstElementChild);
|
|
781
807
|
}
|
|
782
808
|
function refreshSessionAudit(root, session, sessionExport, renderCurrentState) {
|
|
@@ -796,35 +822,41 @@ function producerFeedbackTags(item) {
|
|
|
796
822
|
const tags = item.spec.producerPolicy?.feedbackTags;
|
|
797
823
|
return Array.isArray(tags) ? tags.map(formatValue) : [];
|
|
798
824
|
}
|
|
799
|
-
function renderCandidateCard(candidate, state) {
|
|
825
|
+
function renderCandidateCard(candidate, state, presentationAdapter) {
|
|
800
826
|
const selectedRole = selectedCandidateRole(state);
|
|
801
827
|
const candidateState = state.decision
|
|
802
828
|
? candidate.role === selectedRole ? "selected" : "unselected"
|
|
803
829
|
: "pending";
|
|
804
830
|
const cssState = candidateState === "selected" ? " is-selected" : candidateState === "unselected" ? " is-unselected" : "";
|
|
805
831
|
const confidence = candidate.extraction.confidence ?? candidate.confidence;
|
|
832
|
+
const itemPresentation = buildReviewItemPresentation(state.item, presentationAdapter);
|
|
833
|
+
const presentation = buildReviewCandidatePresentation(state.item, candidate, presentationAdapter, itemPresentation.targetLabel);
|
|
806
834
|
return `
|
|
807
835
|
<article class="candidate-card${cssState}" data-testid="candidate-${escapeHtml(candidate.role ?? candidate.id)}" data-outcome="${candidateState}">
|
|
808
836
|
<div class="card-head">
|
|
809
837
|
<div>
|
|
810
|
-
<p class="eyebrow">${escapeHtml(candidate.
|
|
811
|
-
<h2 class="role">${escapeHtml(
|
|
838
|
+
<p class="eyebrow">${escapeHtml(candidate.role === "proposed" ? "incoming value" : candidate.role === "current" ? "existing value" : "candidate")}</p>
|
|
839
|
+
<h2 class="role">${escapeHtml(presentation.roleLabel)}</h2>
|
|
812
840
|
</div>
|
|
813
841
|
<span class="state-label">${escapeHtml(candidateState)}</span>
|
|
814
842
|
</div>
|
|
815
843
|
<div class="candidate-value">
|
|
816
|
-
<span class="field-label"
|
|
817
|
-
<p class="field-value">${escapeHtml(
|
|
844
|
+
<span class="field-label">${escapeHtml(presentation.valueLabel)}</span>
|
|
845
|
+
<p class="field-value">${escapeHtml(presentation.valueText)}</p>
|
|
818
846
|
</div>
|
|
819
847
|
<dl class="field-stack">
|
|
820
|
-
${fieldItem("Source URL",
|
|
821
|
-
${fieldItem("Source ref", candidate.source.sourceId ?? candidate.source.sourceRef)}
|
|
848
|
+
${fieldItem("Source URL", presentation.sourceText)}
|
|
822
849
|
${fieldItem("Locator", candidate.locator?.locator ?? candidate.locator?.scheme ?? "none")}
|
|
823
850
|
${fieldItem("Excerpt", candidate.locator?.excerpt ?? "none", "excerpt")}
|
|
824
851
|
${fieldItem("Extraction confidence", confidence === undefined ? "unknown" : formatConfidence(confidence))}
|
|
825
852
|
${fieldItem("Extractor", candidate.extraction.extractor ?? "unknown")}
|
|
826
|
-
${fieldItem("Claim", candidate.claimTarget.claimId ?? candidate.claimTarget.fieldOrBehavior)}
|
|
827
853
|
</dl>
|
|
854
|
+
<details class="reference-details">
|
|
855
|
+
<summary>IDs and trace links</summary>
|
|
856
|
+
<dl class="field-stack compact">
|
|
857
|
+
${presentation.traceRefs.map((ref) => fieldItem(ref.label, ref.value)).join("")}
|
|
858
|
+
</dl>
|
|
859
|
+
</details>
|
|
828
860
|
</article>
|
|
829
861
|
`;
|
|
830
862
|
}
|
|
@@ -854,10 +886,6 @@ function fieldItem(label, value, extraClass = "") {
|
|
|
854
886
|
function formatConfidence(value) {
|
|
855
887
|
return `${Math.round(value * 100)}% (${value.toFixed(2)})`;
|
|
856
888
|
}
|
|
857
|
-
function titleCase(value) {
|
|
858
|
-
const displayValue = formatValue(value);
|
|
859
|
-
return displayValue.slice(0, 1).toUpperCase() + displayValue.slice(1);
|
|
860
|
-
}
|
|
861
889
|
function escapeHtml(value) {
|
|
862
890
|
return formatValue(value)
|
|
863
891
|
.replaceAll("&", "&")
|
|
@@ -621,7 +621,7 @@ h1 {
|
|
|
621
621
|
|
|
622
622
|
.candidate-card {
|
|
623
623
|
display: grid;
|
|
624
|
-
grid-template-rows: auto auto minmax(0, 1fr);
|
|
624
|
+
grid-template-rows: auto auto minmax(0, 1fr) auto;
|
|
625
625
|
gap: 11px;
|
|
626
626
|
min-height: auto;
|
|
627
627
|
padding: 14px;
|
|
@@ -667,7 +667,7 @@ h1 {
|
|
|
667
667
|
color: var(--paper-faint);
|
|
668
668
|
font-size: 10px;
|
|
669
669
|
letter-spacing: 0.1em;
|
|
670
|
-
text-transform:
|
|
670
|
+
text-transform: uppercase;
|
|
671
671
|
overflow-wrap: anywhere;
|
|
672
672
|
word-break: break-word;
|
|
673
673
|
}
|
|
@@ -960,6 +960,44 @@ textarea:focus {
|
|
|
960
960
|
color: var(--paper-faint);
|
|
961
961
|
}
|
|
962
962
|
|
|
963
|
+
.reference-details {
|
|
964
|
+
margin-top: 10px;
|
|
965
|
+
border-top: 1px solid var(--line);
|
|
966
|
+
padding-top: 8px;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
.reference-details summary {
|
|
970
|
+
cursor: pointer;
|
|
971
|
+
color: var(--paper-faint);
|
|
972
|
+
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
|
973
|
+
font-size: 10px;
|
|
974
|
+
font-weight: 600;
|
|
975
|
+
letter-spacing: 0.08em;
|
|
976
|
+
text-transform: uppercase;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.reference-details summary::-webkit-details-marker {
|
|
980
|
+
display: none;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
.reference-details summary::before {
|
|
984
|
+
content: "› ";
|
|
985
|
+
color: var(--paper-faint);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
.reference-details[open] summary {
|
|
989
|
+
color: var(--accent);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
.reference-details[open] summary::before {
|
|
993
|
+
content: "⌄ ";
|
|
994
|
+
color: var(--accent);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
.reference-details .field-stack {
|
|
998
|
+
margin-top: 8px;
|
|
999
|
+
}
|
|
1000
|
+
|
|
963
1001
|
/* ---- Decision payload: terminal readout, intentionally demoted -------- */
|
|
964
1002
|
|
|
965
1003
|
.payload-panel {
|
package/package.json
CHANGED