@kontourai/survey 2.2.1 → 2.2.3
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/README.md +14 -6
- package/dist/src/review-workbench/review-queue-session.d.ts +17 -0
- package/dist/src/review-workbench/review-queue-session.js +23 -0
- package/dist/src/review-workbench/review-workbench-css.generated.js +101 -90
- package/dist/src/review-workbench/review-workbench-data.js +67 -5
- package/dist/src/review-workbench/review-workbench.css +101 -90
- package/dist/src/review-workbench/review-workbench.d.ts +1 -1
- package/dist/src/review-workbench/review-workbench.js +104 -80
- package/dist/src/review-workbench/review-workbench.standalone.css +11 -0
- package/example-data/mcp-review-session.json +49 -44
- package/package.json +1 -1
|
@@ -3,149 +3,149 @@
|
|
|
3
3
|
color-scheme: dark;
|
|
4
4
|
|
|
5
5
|
/* structure */
|
|
6
|
-
--k-bg:
|
|
7
|
-
--k-panel:
|
|
8
|
-
--k-panel-raised:
|
|
9
|
-
--k-line:
|
|
10
|
-
--k-line-strong:
|
|
11
|
-
--k-shadow:
|
|
6
|
+
--k-bg: #0a0e13;
|
|
7
|
+
--k-panel: #111824;
|
|
8
|
+
--k-panel-raised: #16202d;
|
|
9
|
+
--k-line: rgba(150, 180, 210, 0.12);
|
|
10
|
+
--k-line-strong: rgba(150, 180, 210, 0.22);
|
|
11
|
+
--k-shadow: 0 26px 60px -42px rgba(0, 0, 0, 0.95);
|
|
12
12
|
|
|
13
13
|
/* text */
|
|
14
|
-
--k-text:
|
|
15
|
-
--k-text-muted:
|
|
16
|
-
--k-text-faint:
|
|
14
|
+
--k-text: #eef3f8;
|
|
15
|
+
--k-text-muted: #aebccb;
|
|
16
|
+
--k-text-faint: #72869b;
|
|
17
17
|
|
|
18
18
|
/* brand */
|
|
19
|
-
--k-brand:
|
|
20
|
-
--k-brand-contrast:
|
|
19
|
+
--k-brand: #5ce0c6;
|
|
20
|
+
--k-brand-contrast: #06080b;
|
|
21
21
|
|
|
22
22
|
/* semantic status scale */
|
|
23
|
-
--k-positive:
|
|
24
|
-
--k-caution:
|
|
25
|
-
--k-negative:
|
|
26
|
-
--k-neutral:
|
|
27
|
-
--k-active:
|
|
23
|
+
--k-positive: #34d399;
|
|
24
|
+
--k-caution: #f3b14b;
|
|
25
|
+
--k-negative: #ff6f6f;
|
|
26
|
+
--k-neutral: #6f8095;
|
|
27
|
+
--k-active: #7aa2ff;
|
|
28
28
|
--k-positive-soft: color-mix(in oklab, var(--k-positive) 14%, transparent);
|
|
29
29
|
--k-caution-soft: color-mix(in oklab, var(--k-caution) 14%, transparent);
|
|
30
30
|
--k-negative-soft: color-mix(in oklab, var(--k-negative) 14%, transparent);
|
|
31
31
|
--k-active-soft: color-mix(in oklab, var(--k-active) 14%, transparent);
|
|
32
32
|
|
|
33
33
|
/* spacing */
|
|
34
|
-
--k-space-1:
|
|
35
|
-
--k-space-2:
|
|
36
|
-
--k-space-3:
|
|
37
|
-
--k-space-4:
|
|
38
|
-
--k-space-5:
|
|
39
|
-
--k-space-6:
|
|
34
|
+
--k-space-1: 4px;
|
|
35
|
+
--k-space-2: 8px;
|
|
36
|
+
--k-space-3: 12px;
|
|
37
|
+
--k-space-4: 16px;
|
|
38
|
+
--k-space-5: 24px;
|
|
39
|
+
--k-space-6: 32px;
|
|
40
40
|
|
|
41
41
|
/* radius */
|
|
42
|
-
--k-radius-sm:
|
|
43
|
-
--k-radius-md:
|
|
42
|
+
--k-radius-sm: 9px;
|
|
43
|
+
--k-radius-md: 14px;
|
|
44
44
|
|
|
45
45
|
/* type */
|
|
46
|
-
--k-text-xs:
|
|
47
|
-
--k-text-sm:
|
|
48
|
-
--k-text-md:
|
|
49
|
-
--k-text-lg:
|
|
50
|
-
--k-text-xl:
|
|
51
|
-
--k-text-2xl:
|
|
46
|
+
--k-text-xs: 11px;
|
|
47
|
+
--k-text-sm: 12.5px;
|
|
48
|
+
--k-text-md: 14px;
|
|
49
|
+
--k-text-lg: 18px;
|
|
50
|
+
--k-text-xl: 22px;
|
|
51
|
+
--k-text-2xl: clamp(26px, 3.4vw, 38px);
|
|
52
52
|
|
|
53
53
|
/* fonts */
|
|
54
|
-
--k-font-display:
|
|
55
|
-
--k-font-ui:
|
|
56
|
-
--k-font-mono:
|
|
54
|
+
--k-font-display: "Fraunces", Georgia, "Times New Roman", serif;
|
|
55
|
+
--k-font-ui: "Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
56
|
+
--k-font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
57
57
|
|
|
58
58
|
/* motion */
|
|
59
|
-
--k-ease:
|
|
60
|
-
--k-dur:
|
|
59
|
+
--k-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
60
|
+
--k-dur: 0.16s;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
.survey-workbench-embed[data-theme="light"]{
|
|
64
64
|
color-scheme: light;
|
|
65
|
-
--k-bg:
|
|
66
|
-
--k-panel:
|
|
67
|
-
--k-panel-raised:
|
|
68
|
-
--k-line:
|
|
69
|
-
--k-line-strong:
|
|
70
|
-
--k-shadow:
|
|
71
|
-
--k-text:
|
|
72
|
-
--k-text-muted:
|
|
73
|
-
--k-text-faint:
|
|
74
|
-
--k-brand-contrast:
|
|
75
|
-
--k-positive:
|
|
76
|
-
--k-caution:
|
|
77
|
-
--k-negative:
|
|
78
|
-
--k-neutral:
|
|
79
|
-
--k-active:
|
|
65
|
+
--k-bg: #f5f4ef;
|
|
66
|
+
--k-panel: #ffffff;
|
|
67
|
+
--k-panel-raised: #fbfaf7;
|
|
68
|
+
--k-line: rgba(36, 40, 46, 0.12);
|
|
69
|
+
--k-line-strong: rgba(36, 40, 46, 0.20);
|
|
70
|
+
--k-shadow: 0 18px 50px -30px rgba(0, 0, 0, 0.25);
|
|
71
|
+
--k-text: #202124;
|
|
72
|
+
--k-text-muted: #5b626b;
|
|
73
|
+
--k-text-faint: #707782;
|
|
74
|
+
--k-brand-contrast: #ffffff;
|
|
75
|
+
--k-positive: #168257;
|
|
76
|
+
--k-caution: #8a5a00;
|
|
77
|
+
--k-negative: #c83b3b;
|
|
78
|
+
--k-neutral: #5f6975;
|
|
79
|
+
--k-active: #3f6fd6;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
.survey-workbench-embed.theme-survey{
|
|
84
|
-
--k-bg:
|
|
85
|
-
--k-brand:
|
|
86
|
-
--k-text-faint:
|
|
84
|
+
--k-bg: #06080b;
|
|
85
|
+
--k-brand: #5ce0c6;
|
|
86
|
+
--k-text-faint: #6f8095;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
.survey-workbench-embed.theme-console{
|
|
90
|
-
--k-font-ui:
|
|
91
|
-
--k-font-display:
|
|
92
|
-
--k-radius-sm:
|
|
93
|
-
--k-radius-md:
|
|
94
|
-
--k-brand:
|
|
95
|
-
--k-brand-contrast:
|
|
96
|
-
--k-bg:
|
|
97
|
-
--k-panel:
|
|
98
|
-
--k-panel-raised:
|
|
99
|
-
--k-line:
|
|
100
|
-
--k-line-strong:
|
|
101
|
-
--k-text:
|
|
102
|
-
--k-text-muted:
|
|
103
|
-
--k-text-faint:
|
|
104
|
-
--k-positive:
|
|
105
|
-
--k-caution:
|
|
106
|
-
--k-negative:
|
|
107
|
-
--k-active:
|
|
90
|
+
--k-font-ui: "Aptos Narrow", "DIN Condensed", "IBM Plex Sans Condensed", "Gill Sans", sans-serif;
|
|
91
|
+
--k-font-display: "Aptos Narrow", "DIN Condensed", "IBM Plex Sans Condensed", "Gill Sans", sans-serif;
|
|
92
|
+
--k-radius-sm: 0;
|
|
93
|
+
--k-radius-md: 0;
|
|
94
|
+
--k-brand: #c9ff4a;
|
|
95
|
+
--k-brand-contrast: #11120f;
|
|
96
|
+
--k-bg: #11120f;
|
|
97
|
+
--k-panel: #191b16;
|
|
98
|
+
--k-panel-raised: #20231e;
|
|
99
|
+
--k-line: #3a4035;
|
|
100
|
+
--k-line-strong: #8ea36e;
|
|
101
|
+
--k-text: #f2f0e8;
|
|
102
|
+
--k-text-muted: #a6ab9c;
|
|
103
|
+
--k-text-faint: #747a6c;
|
|
104
|
+
--k-positive: #a6d37b;
|
|
105
|
+
--k-caution: #e8c15f;
|
|
106
|
+
--k-negative: #ee776f;
|
|
107
|
+
--k-active: #84d8c8;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
.survey-workbench-embed.theme-flow{
|
|
111
|
-
--k-brand:
|
|
111
|
+
--k-brand: #2f88a6;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
.survey-workbench-embed.theme-surface{
|
|
115
|
-
--k-brand:
|
|
115
|
+
--k-brand: #14a37a;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
.survey-workbench-embed[data-theme="light"].theme-survey,
|
|
119
119
|
.survey-workbench-embed[data-theme="light"] .theme-survey{
|
|
120
|
-
--k-brand:
|
|
120
|
+
--k-brand: #16806f;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
.survey-workbench-embed[data-theme="light"].theme-console,
|
|
124
124
|
.survey-workbench-embed[data-theme="light"] .theme-console{
|
|
125
|
-
--k-brand:
|
|
126
|
-
--k-brand-contrast:
|
|
127
|
-
--k-bg:
|
|
128
|
-
--k-panel:
|
|
129
|
-
--k-panel-raised:
|
|
130
|
-
--k-line:
|
|
131
|
-
--k-line-strong:
|
|
132
|
-
--k-text:
|
|
133
|
-
--k-text-muted:
|
|
134
|
-
--k-text-faint:
|
|
135
|
-
--k-positive:
|
|
136
|
-
--k-caution:
|
|
137
|
-
--k-negative:
|
|
138
|
-
--k-active:
|
|
125
|
+
--k-brand: #6c9400;
|
|
126
|
+
--k-brand-contrast: #ffffff;
|
|
127
|
+
--k-bg: #f3f5eb;
|
|
128
|
+
--k-panel: #fbfcf7;
|
|
129
|
+
--k-panel-raised: #eef2e6;
|
|
130
|
+
--k-line: #ccd5bf;
|
|
131
|
+
--k-line-strong: #8fa36f;
|
|
132
|
+
--k-text: #1e2319;
|
|
133
|
+
--k-text-muted: #596250;
|
|
134
|
+
--k-text-faint: #77816d;
|
|
135
|
+
--k-positive: #2f7d32;
|
|
136
|
+
--k-caution: #8a6500;
|
|
137
|
+
--k-negative: #b93a36;
|
|
138
|
+
--k-active: #247f75;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
.survey-workbench-embed[data-theme="light"].theme-flow,
|
|
142
142
|
.survey-workbench-embed[data-theme="light"] .theme-flow{
|
|
143
|
-
--k-brand:
|
|
143
|
+
--k-brand: #1f6f88;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
.survey-workbench-embed[data-theme="light"].theme-surface,
|
|
147
147
|
.survey-workbench-embed[data-theme="light"] .theme-surface{
|
|
148
|
-
--k-brand:
|
|
148
|
+
--k-brand: #0f6b52;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
|
|
@@ -399,6 +399,17 @@
|
|
|
399
399
|
transition: width 0.35s cubic-bezier(0.2, 0.7, 0.3, 1);
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
+
/* `.progress` is a generic class name, and a host page may well have its own —
|
|
403
|
+
@kontourai/ui ships an unscoped `.progress span { background: linear-gradient(…) }`
|
|
404
|
+
that painted a gradient straight over this element's text in an embed. The
|
|
405
|
+
embed cannot claim the name, but it must not leave its own descendants
|
|
406
|
+
unstyled and open to a host rule; state the presentation explicitly. */
|
|
407
|
+
.survey-workbench-embed .progress .ptext{
|
|
408
|
+
display: inline;
|
|
409
|
+
height: auto;
|
|
410
|
+
background: none;
|
|
411
|
+
}
|
|
412
|
+
|
|
402
413
|
.survey-workbench-embed .ptext{
|
|
403
414
|
font-size: 13px;
|
|
404
415
|
color: var(--k-muted);
|
|
@@ -3,7 +3,7 @@ import { type ReviewSessionReplayIssue } from "./review-session-replay.js";
|
|
|
3
3
|
import { type ReviewPresentationAdapter } from "./review-presentation.js";
|
|
4
4
|
import { type ReviewCandidate, type ReviewDecision, type ReviewItem, type ReviewSession, type ReviewSessionEvent, type ReviewValueDescriptor } from "../review-resource.js";
|
|
5
5
|
export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, mountExtractionInspector, type ExtractionAlignmentState, type ArtifactUnavailableCode, type ExtractionInspectorCandidate, type ExtractionInspectorEntry, type ExtractionInspectorExportOptions, type ExtractionInspectorFilters, type ExtractionInspectorInput, type ExtractionInspectorMountOptions, type ExtractionInspectorModel, type ExtractionInspectorSource, type ResolvedExtractionArtifact, } from "./extraction-inspector.js";
|
|
6
|
-
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";
|
|
6
|
+
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, keepActionDecision, 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";
|
|
7
7
|
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";
|
|
8
8
|
export { buildSurfaceProjectionPreview, type PreviewAuthorityTrace, type PreviewCandidateHistory, type PreviewClaim, type PreviewIntegrityPosture, type PreviewReviewEvent, type PreviewSourceAuthority, type PreviewSourceEvidence, type SurfaceProjectionPreview, } from "./review-surface-preview.js";
|
|
9
9
|
export { validateReviewSessionEventsForSnapshot, type ReviewSessionReplayIssue, type ReviewSessionReplayIssueCode, } from "./review-session-replay.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { canonicalJson } from "./canonical.js";
|
|
2
2
|
import { assertReviewResolutionConsistency } from "../producer-discipline.js";
|
|
3
|
-
import { candidateForDecision, buildReviewSessionEvent, buildReviewSessionEvents, buildReviewSessionResource, currentReviewWorkbenchState, defaultReviewSessionName, effectiveValueForDecision, initialReviewQueueSessionState, replayReviewSessionEvents, reviewWorkbenchSessionStorageKey, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
3
|
+
import { candidateForDecision, keepActionDecision, buildReviewSessionEvent, buildReviewSessionEvents, buildReviewSessionResource, currentReviewWorkbenchState, defaultReviewSessionName, effectiveValueForDecision, initialReviewQueueSessionState, replayReviewSessionEvents, reviewWorkbenchSessionStorageKey, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
4
4
|
import { validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
|
|
5
5
|
import { buildSurfaceProjectionPreview, formatValue, } from "./review-surface-preview.js";
|
|
6
6
|
import { buildReviewCandidatePresentation, buildReviewItemPresentation, } from "./review-presentation.js";
|
|
@@ -8,7 +8,7 @@ import { reviewResourceApiVersion, } from "../review-resource.js";
|
|
|
8
8
|
import { validateAuthorizing, buildAuthorizedActionAuthorizing } from "../review-authorizing.js";
|
|
9
9
|
import { humanizeIdentifier } from "./review-presentation.js";
|
|
10
10
|
export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, mountExtractionInspector, } from "./extraction-inspector.js";
|
|
11
|
-
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
11
|
+
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, keepActionDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
12
12
|
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-presentation.js";
|
|
13
13
|
export { buildSurfaceProjectionPreview, } from "./review-surface-preview.js";
|
|
14
14
|
export { validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
|
|
@@ -475,11 +475,19 @@ function fieldCardState(item, decision) {
|
|
|
475
475
|
return "kept";
|
|
476
476
|
return "review";
|
|
477
477
|
}
|
|
478
|
-
|
|
478
|
+
/**
|
|
479
|
+
* The chip copy for a decided field. An item with no current value has nothing
|
|
480
|
+
* to "keep": both the kept and rejected outcomes leave it unset, and for such an
|
|
481
|
+
* item the recorded decision genuinely does not distinguish "leave unset" from
|
|
482
|
+
* "leave unset, and the suggestion was wrong" (both are `reject-proposed` — see
|
|
483
|
+
* {@link keepActionDecision}). Saying "Kept — flagged wrong" there would claim a
|
|
484
|
+
* distinction the record does not hold, so the chip states only what happened.
|
|
485
|
+
*/
|
|
486
|
+
function chipLabel(state, hasCurrentValue) {
|
|
479
487
|
switch (state) {
|
|
480
488
|
case "accepted": return "Accepted";
|
|
481
|
-
case "kept": return "Kept current";
|
|
482
|
-
case "rejected": return "Kept — flagged wrong";
|
|
489
|
+
case "kept": return hasCurrentValue ? "Kept current" : "Left unset";
|
|
490
|
+
case "rejected": return hasCurrentValue ? "Kept — flagged wrong" : "Left unset";
|
|
483
491
|
case "could-not-confirm": return "Could not confirm";
|
|
484
492
|
default: return "Needs review";
|
|
485
493
|
}
|
|
@@ -607,6 +615,11 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
607
615
|
const hasCurrentValue = current !== undefined && !isEmptyValue(current.value);
|
|
608
616
|
const kind = hasCurrentValue ? "Update" : "New";
|
|
609
617
|
const keepLabel = hasCurrentValue ? "Keep current" : "Leave unset";
|
|
618
|
+
// Only render controls whose decision this item can actually record. Every
|
|
619
|
+
// decision resolves to a candidate role, so a control routed at a role the
|
|
620
|
+
// item does not carry cannot emit anything — it used to throw mid-click and
|
|
621
|
+
// leave the queue unrenderable (kontourai/survey#203).
|
|
622
|
+
const keepDecision = keepActionDecision(item, false);
|
|
610
623
|
const editedValue = session.editedValuesByItemName?.[item.metadata.name];
|
|
611
624
|
const proposedPresentationText = proposed
|
|
612
625
|
? buildReviewCandidatePresentation(item, proposed, presentationAdapter, presentation.targetLabel).valueText
|
|
@@ -632,23 +645,23 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
632
645
|
<div class="frow1">
|
|
633
646
|
<span class="fname">${escapeHtml(presentation.targetLabel)}</span>
|
|
634
647
|
<span class="fkind">${kind}</span>
|
|
635
|
-
<span class="chip ${state} push" data-testid="field-chip">${chipLabel(state)}</span>
|
|
648
|
+
<span class="chip ${state} push" data-testid="field-chip">${chipLabel(state, hasCurrentValue)}</span>
|
|
636
649
|
</div>
|
|
637
650
|
${proposed
|
|
638
651
|
? renderDiffRow(item, current, proposed, presentation.targetLabel, decided, effectiveProposedText, currentPresentationText)
|
|
639
652
|
: "<p class=\"field-value\">No proposed value is available for this field.</p>"}
|
|
640
653
|
${proposed ? renderProvenanceRow(item, proposed, presentationAdapter) : ""}
|
|
641
654
|
<div class="decide">
|
|
642
|
-
|
|
643
|
-
|
|
655
|
+
${keepDecision === undefined ? "" : `<button class="btn keep" type="button" data-testid="keep-current" data-item-name="${escapeHtml(item.metadata.name)}">${keepLabel}</button>`}
|
|
656
|
+
${proposed ? `<button class="btn use" type="button" data-testid="use-proposed" data-item-name="${escapeHtml(item.metadata.name)}">Use proposed</button>
|
|
644
657
|
<label class="wrong">
|
|
645
658
|
<input type="checkbox" class="wrongbox" data-testid="wrong-toggle" data-item-name="${escapeHtml(item.metadata.name)}">
|
|
646
659
|
Suggestion was wrong
|
|
647
660
|
</label>
|
|
648
|
-
<button class="btn unconfirmed" type="button" data-testid="could-not-confirm" data-item-name="${escapeHtml(item.metadata.name)}">Could not confirm</button
|
|
661
|
+
<button class="btn unconfirmed" type="button" data-testid="could-not-confirm" data-item-name="${escapeHtml(item.metadata.name)}">Could not confirm</button>` : ""}
|
|
649
662
|
</div>
|
|
650
663
|
<div class="decided">
|
|
651
|
-
<span class="chip ${state}" data-testid="decided-chip">${chipLabel(state)}</span>
|
|
664
|
+
<span class="chip ${state}" data-testid="decided-chip">${chipLabel(state, hasCurrentValue)}</span>
|
|
652
665
|
<button class="undo" type="button" data-testid="undo-decision" data-item-name="${escapeHtml(item.metadata.name)}">Change</button>
|
|
653
666
|
</div>
|
|
654
667
|
${renderAuditDetails(item, current, proposed, session, presentationAdapter)}
|
|
@@ -1070,7 +1083,16 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
1070
1083
|
const persistEvents = () => {
|
|
1071
1084
|
eventStore?.save(session, events);
|
|
1072
1085
|
};
|
|
1073
|
-
|
|
1086
|
+
/**
|
|
1087
|
+
* Builds the session event for a state WITHOUT committing anything. Callers
|
|
1088
|
+
* that also mutate the session build the event first and commit only once it
|
|
1089
|
+
* exists: a decision the item cannot represent must leave the workbench
|
|
1090
|
+
* exactly as it was, not half-applied and unrenderable (kontourai/survey#203).
|
|
1091
|
+
*/
|
|
1092
|
+
const buildEventForSession = (sessionForEvent, eventType, itemName, dataOverride) => {
|
|
1093
|
+
// Shadow the controller's mutable `session` so this builder can only read
|
|
1094
|
+
// the state it was handed.
|
|
1095
|
+
const session = sessionForEvent;
|
|
1074
1096
|
const item = itemName ? session.items.find((entry) => entry.metadata.name === itemName) : undefined;
|
|
1075
1097
|
const decision = itemName ? session.decisionsByItemName[itemName] : undefined;
|
|
1076
1098
|
const candidate = item && decision ? candidateForDecision(item, decision) : undefined;
|
|
@@ -1089,83 +1111,71 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
1089
1111
|
? { workbenchDecision: decision, workbenchEditedValue: editedValue }
|
|
1090
1112
|
: { workbenchDecision: decision, ...(attemptEvidenceIds?.length ? { attemptEvidenceIds } : {}) })
|
|
1091
1113
|
: undefined;
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
data: dataOverride ?? defaultData,
|
|
1112
|
-
}),
|
|
1113
|
-
];
|
|
1114
|
+
return buildReviewSessionEvent(session, {
|
|
1115
|
+
sessionName: defaultReviewSessionName,
|
|
1116
|
+
sequence: events.length + 1,
|
|
1117
|
+
eventType,
|
|
1118
|
+
occurredAt: session.reviewedAt,
|
|
1119
|
+
reviewItemName: itemName,
|
|
1120
|
+
reviewDecisionName: item && decision ? `${item.metadata.name}-${decision}` : undefined,
|
|
1121
|
+
candidateId: candidate?.id,
|
|
1122
|
+
status: definition?.status,
|
|
1123
|
+
...(decision === "could-not-confirm"
|
|
1124
|
+
? {
|
|
1125
|
+
resolution: "could_not_confirm",
|
|
1126
|
+
resolutionReason: note?.trim(),
|
|
1127
|
+
...(attemptEvidenceIds ? { attemptEvidenceIds: [...attemptEvidenceIds] } : {}),
|
|
1128
|
+
}
|
|
1129
|
+
: {}),
|
|
1130
|
+
rationale: note,
|
|
1131
|
+
data: dataOverride ?? defaultData,
|
|
1132
|
+
});
|
|
1114
1133
|
};
|
|
1115
|
-
|
|
1116
|
-
|
|
1134
|
+
/** Commits a session update and its event together, or neither. */
|
|
1135
|
+
const commitSessionUpdate = (nextSession, eventType, itemName, dataOverride) => {
|
|
1136
|
+
const event = buildEventForSession(nextSession, eventType, itemName, dataOverride);
|
|
1137
|
+
session = nextSession;
|
|
1138
|
+
events = [...events, event];
|
|
1139
|
+
persistEvents();
|
|
1117
1140
|
};
|
|
1118
1141
|
const setDecision = (itemName, decision, rawEditedValue) => {
|
|
1119
1142
|
if (decision === "could-not-confirm" && !session.notesByItemName[itemName]?.trim()) {
|
|
1120
1143
|
throw new Error("Could not confirm requires a non-empty reason.");
|
|
1121
1144
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
};
|
|
1139
|
-
});
|
|
1140
|
-
appendEvent("decision-changed", itemName);
|
|
1141
|
-
persistEvents();
|
|
1145
|
+
const item = session.items.find((entry) => entry.metadata.name === itemName);
|
|
1146
|
+
const proposed = item?.spec.candidates.find((candidate) => candidate.role === "proposed");
|
|
1147
|
+
const originalText = proposed ? formatValue(proposed.value) : undefined;
|
|
1148
|
+
const nextEditedValuesByItemName = { ...session.editedValuesByItemName };
|
|
1149
|
+
if (decision === "accept-proposed" && rawEditedValue !== undefined && rawEditedValue !== originalText) {
|
|
1150
|
+
nextEditedValuesByItemName[itemName] = rawEditedValue;
|
|
1151
|
+
}
|
|
1152
|
+
else {
|
|
1153
|
+
delete nextEditedValuesByItemName[itemName];
|
|
1154
|
+
}
|
|
1155
|
+
commitSessionUpdate({
|
|
1156
|
+
...session,
|
|
1157
|
+
activeItemName: itemName,
|
|
1158
|
+
decisionsByItemName: { ...session.decisionsByItemName, [itemName]: decision },
|
|
1159
|
+
editedValuesByItemName: nextEditedValuesByItemName,
|
|
1160
|
+
}, "decision-changed", itemName);
|
|
1142
1161
|
};
|
|
1143
1162
|
const clearDecision = (itemName) => {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
};
|
|
1158
|
-
});
|
|
1159
|
-
appendEvent("decision-changed", itemName, { workbenchDecision: null });
|
|
1160
|
-
persistEvents();
|
|
1163
|
+
const remainingDecisions = { ...session.decisionsByItemName };
|
|
1164
|
+
delete remainingDecisions[itemName];
|
|
1165
|
+
const remainingEdits = { ...session.editedValuesByItemName };
|
|
1166
|
+
delete remainingEdits[itemName];
|
|
1167
|
+
const remainingAttemptEvidenceIds = { ...session.attemptEvidenceIdsByItemName };
|
|
1168
|
+
delete remainingAttemptEvidenceIds[itemName];
|
|
1169
|
+
commitSessionUpdate({
|
|
1170
|
+
...session,
|
|
1171
|
+
activeItemName: itemName,
|
|
1172
|
+
decisionsByItemName: remainingDecisions,
|
|
1173
|
+
editedValuesByItemName: remainingEdits,
|
|
1174
|
+
attemptEvidenceIdsByItemName: remainingAttemptEvidenceIds,
|
|
1175
|
+
}, "decision-changed", itemName, { workbenchDecision: null });
|
|
1161
1176
|
};
|
|
1162
1177
|
const updateReviewerNote = (itemName, note) => {
|
|
1163
|
-
|
|
1164
|
-
...current,
|
|
1165
|
-
notesByItemName: { ...current.notesByItemName, [itemName]: note },
|
|
1166
|
-
}));
|
|
1167
|
-
appendEvent("note-changed", itemName);
|
|
1168
|
-
persistEvents();
|
|
1178
|
+
commitSessionUpdate({ ...session, notesByItemName: { ...session.notesByItemName, [itemName]: note } }, "note-changed", itemName);
|
|
1169
1179
|
};
|
|
1170
1180
|
const controller = {
|
|
1171
1181
|
currentSession: () => session,
|
|
@@ -1252,7 +1262,13 @@ function bindFieldCardInteractions(root, controller) {
|
|
|
1252
1262
|
const descriptor = controller
|
|
1253
1263
|
.currentSession()
|
|
1254
1264
|
.items.find((entry) => entry.metadata.name === itemName)?.spec.valueDescriptor;
|
|
1255
|
-
|
|
1265
|
+
// Validate the reviewer's edit only when there IS an editor to have edited.
|
|
1266
|
+
// A non-editable item (`spec.editable === false`, e.g. every envelope-imported
|
|
1267
|
+
// item) renders no editor, so there is no reviewer input to check — validating
|
|
1268
|
+
// the absent editor's "" value blocked the decision on every typed field, and
|
|
1269
|
+
// wrote the reason into an error slot inside the suppressed editor, so the
|
|
1270
|
+
// reviewer saw nothing at all (kontourai/survey#201).
|
|
1271
|
+
const error = input ? validateProposedValue(descriptor, input.value) : undefined;
|
|
1256
1272
|
const errorEl = field?.querySelector("[data-testid='value-error']");
|
|
1257
1273
|
if (error) {
|
|
1258
1274
|
// Block the decision and surface the reason inline; the transient DOM
|
|
@@ -1277,7 +1293,15 @@ function bindFieldCardInteractions(root, controller) {
|
|
|
1277
1293
|
const itemName = button.dataset.itemName ?? "";
|
|
1278
1294
|
const field = button.closest("[data-testid='review-field']");
|
|
1279
1295
|
const wrong = field?.querySelector("[data-testid='wrong-toggle']");
|
|
1280
|
-
const
|
|
1296
|
+
const item = controller.currentSession().items.find((entry) => entry.metadata.name === itemName);
|
|
1297
|
+
// Route by what this item can record, not by the button's identity: on an
|
|
1298
|
+
// item with no current candidate this control reads "Leave unset", and
|
|
1299
|
+
// "keep the current candidate" is not a decision it can carry
|
|
1300
|
+
// (kontourai/survey#203).
|
|
1301
|
+
const decision = item ? keepActionDecision(item, wrong?.checked ?? false) : undefined;
|
|
1302
|
+
if (!decision) {
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1281
1305
|
controller.setDecision(itemName, decision);
|
|
1282
1306
|
controller.renderCurrentState();
|
|
1283
1307
|
});
|
|
@@ -248,6 +248,17 @@ h1, h2, h3, p {
|
|
|
248
248
|
transition: width 0.35s cubic-bezier(0.2, 0.7, 0.3, 1);
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/* `.progress` is a generic class name, and a host page may well have its own —
|
|
252
|
+
@kontourai/ui ships an unscoped `.progress span { background: linear-gradient(…) }`
|
|
253
|
+
that painted a gradient straight over this element's text in an embed. The
|
|
254
|
+
embed cannot claim the name, but it must not leave its own descendants
|
|
255
|
+
unstyled and open to a host rule; state the presentation explicitly. */
|
|
256
|
+
.progress .ptext {
|
|
257
|
+
display: inline;
|
|
258
|
+
height: auto;
|
|
259
|
+
background: none;
|
|
260
|
+
}
|
|
261
|
+
|
|
251
262
|
.ptext {
|
|
252
263
|
font-size: 13px;
|
|
253
264
|
color: var(--k-muted);
|