@happyvertical/smrt-ingestion 0.57.21 → 0.57.22
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/FEEDBACK.md +104 -0
- package/PROPOSALS.md +19 -0
- package/README.md +126 -0
- package/REVIEW.md +272 -4
- package/agents/execution.md +72 -0
- package/dist/chunks/{execution-9CfjxX8I.js → execution-BzVQax0n.js} +421 -23
- package/dist/chunks/execution-BzVQax0n.js.map +1 -0
- package/dist/chunks/{feedback-CuUzWKzP.js → feedback-D3UTF8fm.js} +119 -21
- package/dist/chunks/feedback-D3UTF8fm.js.map +1 -0
- package/dist/chunks/{extraction-CJ-zrbg0.js → proposal-contracts-77Uo1B1j.js} +39 -39
- package/dist/chunks/proposal-contracts-77Uo1B1j.js.map +1 -0
- package/dist/chunks/{reviewer-instruction-Db3jDN9a.js → reviewer-instruction-CEc3AOuU.js} +2 -2
- package/dist/chunks/{reviewer-instruction-Db3jDN9a.js.map → reviewer-instruction-CEc3AOuU.js.map} +1 -1
- package/dist/dto.d.ts +69 -0
- package/dist/dto.d.ts.map +1 -1
- package/dist/execution-contracts.d.ts +4 -1
- package/dist/execution-contracts.d.ts.map +1 -1
- package/dist/execution-dto.d.ts +16 -0
- package/dist/execution-dto.d.ts.map +1 -1
- package/dist/execution.d.ts +58 -3
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js +2 -2
- package/dist/feedback-dto.d.ts +12 -4
- package/dist/feedback-dto.d.ts.map +1 -1
- package/dist/feedback-internal.d.ts +9 -0
- package/dist/feedback-internal.d.ts.map +1 -1
- package/dist/feedback.d.ts +3 -0
- package/dist/feedback.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/manifest.json +321 -3
- package/dist/models.d.ts +15 -0
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +73 -2
- package/dist/models.js.map +1 -1
- package/dist/policy.d.ts +3 -0
- package/dist/policy.d.ts.map +1 -1
- package/dist/proposal-errors.d.ts +4 -0
- package/dist/proposal-errors.d.ts.map +1 -1
- package/dist/proposals.d.ts +1 -0
- package/dist/proposals.d.ts.map +1 -1
- package/dist/proposals.js +11 -6
- package/dist/proposals.js.map +1 -1
- package/dist/redaction.d.ts +9 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/review-dto.d.ts +14 -4
- package/dist/review-dto.d.ts.map +1 -1
- package/dist/review-host.d.ts.map +1 -1
- package/dist/server.d.ts +82 -10
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +366 -47
- package/dist/server.js.map +1 -1
- package/dist/smrt-knowledge.json +124 -18
- package/dist/svelte/components/ActionReview.svelte +39 -9
- package/dist/svelte/components/ActionReview.svelte.d.ts +8 -1
- package/dist/svelte/components/ActionReview.svelte.d.ts.map +1 -1
- package/dist/svelte/components/EvidenceViewer.svelte +60 -4
- package/dist/svelte/components/EvidenceViewer.svelte.d.ts +12 -1
- package/dist/svelte/components/EvidenceViewer.svelte.d.ts.map +1 -1
- package/dist/svelte/components/InboxView.svelte +160 -18
- package/dist/svelte/components/InboxView.svelte.d.ts +28 -1
- package/dist/svelte/components/InboxView.svelte.d.ts.map +1 -1
- package/dist/svelte/components/IntakeWorkspace.svelte +27 -0
- package/dist/svelte/components/IntakeWorkspace.svelte.d.ts +19 -0
- package/dist/svelte/components/IntakeWorkspace.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ReviewView.svelte +264 -23
- package/dist/svelte/components/ReviewView.svelte.d.ts +18 -1
- package/dist/svelte/components/ReviewView.svelte.d.ts.map +1 -1
- package/dist/svelte/components/TriageView.svelte +669 -0
- package/dist/svelte/components/TriageView.svelte.d.ts +16 -0
- package/dist/svelte/components/TriageView.svelte.d.ts.map +1 -0
- package/dist/svelte/i18n.d.ts +39 -0
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +39 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +1 -0
- package/dist/svelte/view-utils.d.ts +65 -0
- package/dist/svelte/view-utils.d.ts.map +1 -1
- package/dist/svelte/view-utils.js +145 -0
- package/package.json +13 -13
- package/dist/chunks/execution-9CfjxX8I.js.map +0 -1
- package/dist/chunks/extraction-CJ-zrbg0.js.map +0 -1
- package/dist/chunks/feedback-CuUzWKzP.js.map +0 -1
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { Textarea } from '@happyvertical/smrt-ui/forms';
|
|
2
|
+
import { Input, Select, Textarea } from '@happyvertical/smrt-ui/forms';
|
|
3
3
|
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
4
4
|
import { Button } from '@happyvertical/smrt-ui/ui';
|
|
5
|
-
import { onDestroy, onMount } from 'svelte';
|
|
5
|
+
import { onDestroy, onMount, untrack } from 'svelte';
|
|
6
6
|
import type { EvidenceLocation } from '../../extraction-types.js';
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
GeneratedSuggestion,
|
|
9
|
+
ProposalCatalogEntry,
|
|
10
|
+
} from '../../proposal-dto.js';
|
|
8
11
|
import type { IntakeReviewHost, ItemReviewView } from '../../review-dto.js';
|
|
9
12
|
import { M } from '../i18n.js';
|
|
10
13
|
import {
|
|
11
14
|
argumentsObject,
|
|
15
|
+
draftArguments,
|
|
12
16
|
evidenceUrl,
|
|
13
17
|
extractedContent,
|
|
18
|
+
missingArguments,
|
|
14
19
|
missingRequired,
|
|
20
|
+
observedPages,
|
|
15
21
|
pageGroups,
|
|
22
|
+
reviewInFlight,
|
|
16
23
|
} from '../view-utils.js';
|
|
17
|
-
import ActionReview from './ActionReview.svelte';
|
|
24
|
+
import ActionReview, { type ReviewOutcome } from './ActionReview.svelte';
|
|
18
25
|
import CandidatePicker from './CandidatePicker.svelte';
|
|
19
26
|
import EvidenceViewer from './EvidenceViewer.svelte';
|
|
20
27
|
export interface Props {
|
|
@@ -22,8 +29,37 @@ export interface Props {
|
|
|
22
29
|
itemId: string;
|
|
23
30
|
/** Reload interval while the host reports the item as processing. */
|
|
24
31
|
pollMs?: number;
|
|
32
|
+
/** `split` puts the original beside the form; `stacked` puts it above, one part at a time. */
|
|
33
|
+
layout?: 'split' | 'stacked';
|
|
34
|
+
/**
|
|
35
|
+
* Every displayed view (load, in-place poll patch, denial projection), and
|
|
36
|
+
* `undefined` when a displayed view is cleared for a reload or an operation
|
|
37
|
+
* whose result has not been shown yet. `denied` is true when it was cleared
|
|
38
|
+
* because a callback failed (the view fails closed), not for a reload.
|
|
39
|
+
* Presentation only.
|
|
40
|
+
*/
|
|
41
|
+
onview?: (
|
|
42
|
+
view: ItemReviewView | undefined,
|
|
43
|
+
denied: boolean,
|
|
44
|
+
issued: number,
|
|
45
|
+
) => void;
|
|
46
|
+
/** Numbers each load when its request starts; reported as `issued` with the view. */
|
|
47
|
+
stamp?: () => number;
|
|
48
|
+
/** Whether the reviewer has unsent input on this item. */
|
|
49
|
+
ondirty?: (dirty: boolean) => void;
|
|
50
|
+
/** After an approve, apply or resolve and the reload that follows it succeeded. */
|
|
51
|
+
onoutcome?: (outcome: ReviewOutcome, view: ItemReviewView) => void;
|
|
25
52
|
}
|
|
26
|
-
let {
|
|
53
|
+
let {
|
|
54
|
+
host,
|
|
55
|
+
itemId,
|
|
56
|
+
pollMs = 2000,
|
|
57
|
+
layout = 'split',
|
|
58
|
+
onview,
|
|
59
|
+
stamp,
|
|
60
|
+
ondirty,
|
|
61
|
+
onoutcome,
|
|
62
|
+
}: Props = $props();
|
|
27
63
|
const { t } = useI18n();
|
|
28
64
|
let view = $state<ItemReviewView>(),
|
|
29
65
|
pending = $state(false),
|
|
@@ -71,16 +107,83 @@ onDestroy(() => {
|
|
|
71
107
|
epoch++;
|
|
72
108
|
clearTimeout(poll);
|
|
73
109
|
});
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
110
|
+
// Includes the server's own predicate, which also covers the gap between a
|
|
111
|
+
// completed extraction and its not-yet-appended interpretation.
|
|
112
|
+
const inFlight = reviewInFlight;
|
|
113
|
+
// Parents follow the displayed view (a workspace keeps its list row in step);
|
|
114
|
+
// untracked so the callback's own reads never re-run this effect. Undefined
|
|
115
|
+
// means a displayed view was cleared for a reload or an operation.
|
|
116
|
+
let shown = false;
|
|
117
|
+
// The read sequence number of the load that produced the displayed view.
|
|
118
|
+
let viewIssued = 0;
|
|
119
|
+
$effect(() => {
|
|
120
|
+
const current = view;
|
|
121
|
+
// Read too, so a denial after an operation cleared the view still reports.
|
|
122
|
+
const denied = !current && error;
|
|
123
|
+
// A first load that failed reports its denial too.
|
|
124
|
+
if (!current && !shown && !denied) return;
|
|
125
|
+
shown = true;
|
|
126
|
+
untrack(() => onview?.(current, denied, viewIssued));
|
|
127
|
+
});
|
|
128
|
+
// Unsent reviewer input on this item (instruction, drafts, split edits,
|
|
129
|
+
// start/resolve fields, action editors); a workspace confirms before leaving.
|
|
130
|
+
// It is read from the drafts themselves, against the text each draft started
|
|
131
|
+
// from, so it holds while a reload has cleared the view. While the view is
|
|
132
|
+
// denied nothing is shown, so nothing counts: the reviewer is never asked to
|
|
133
|
+
// discard input they cannot see (it is kept, and returns with an authorized
|
|
134
|
+
// refresh, as before).
|
|
135
|
+
let actionDirty = $state<Record<string, boolean>>({}),
|
|
136
|
+
// The generated text each suggestion draft and split draft started from.
|
|
137
|
+
draftBase = $state<Record<string, string>>({}),
|
|
138
|
+
splitBase = $state<Record<string, string>>({});
|
|
139
|
+
const dirty = $derived(
|
|
140
|
+
(view ? view.availability === 'available' : !error) &&
|
|
141
|
+
(!!instruction.trim() ||
|
|
142
|
+
editedFields() ||
|
|
143
|
+
Object.values(actionDirty).some(Boolean) ||
|
|
144
|
+
Object.entries(drafts).some(([key, draft]) => draft !== draftBase[key]) ||
|
|
145
|
+
Object.entries(splits).some(([id, draft]) => draft !== splitBase[id])),
|
|
146
|
+
);
|
|
147
|
+
/** Start-action and resolve fields (declared with their controls below). */
|
|
148
|
+
function editedFields() {
|
|
149
|
+
return !!resolveReason || !!startHandler || !!startReason;
|
|
83
150
|
}
|
|
151
|
+
// Split drafts start from the generation's groups whenever a view shows them.
|
|
152
|
+
$effect(() => {
|
|
153
|
+
const current = view;
|
|
154
|
+
if (current)
|
|
155
|
+
splitBase = Object.fromEntries(
|
|
156
|
+
(current.generation?.splits ?? []).map((split) => [
|
|
157
|
+
split.evidenceId,
|
|
158
|
+
JSON.stringify(split.groups),
|
|
159
|
+
]),
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
// A closed item takes no instruction, suggestion, split or started action, so
|
|
163
|
+
// such input left when it closed can never be sent: it is discarded rather
|
|
164
|
+
// than kept as unsent work.
|
|
165
|
+
$effect(() => {
|
|
166
|
+
if (view?.availability !== 'available' || !view.entry.item.disposition)
|
|
167
|
+
return;
|
|
168
|
+
untrack(() => {
|
|
169
|
+
instruction = '';
|
|
170
|
+
drafts = {};
|
|
171
|
+
draftBase = {};
|
|
172
|
+
startHandler = '';
|
|
173
|
+
startArgs = '';
|
|
174
|
+
startReason = '';
|
|
175
|
+
splits = {};
|
|
176
|
+
resolveReason = '';
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
$effect(() => {
|
|
180
|
+
const current = dirty;
|
|
181
|
+
untrack(() => ondirty?.(current));
|
|
182
|
+
});
|
|
183
|
+
// The original's chosen part and pages outlive in-place reloads and
|
|
184
|
+
// operations (the viewer itself unmounts while a reload is pending).
|
|
185
|
+
let viewerPart = $state<string>(),
|
|
186
|
+
viewerPages = $state<Record<string, number>>({});
|
|
84
187
|
/** Prefer the server's retry binding; otherwise the displayed generation. */
|
|
85
188
|
function instructionBinding(current: ItemReviewView | undefined) {
|
|
86
189
|
if (current?.instruction?.binding) return current.instruction.binding;
|
|
@@ -121,7 +224,10 @@ function deny() {
|
|
|
121
224
|
async function fetchView(
|
|
122
225
|
ticket: number,
|
|
123
226
|
pageCount: number,
|
|
124
|
-
): Promise<
|
|
227
|
+
): Promise<
|
|
228
|
+
{ result: ItemReviewView; pages: number; issued: number } | undefined
|
|
229
|
+
> {
|
|
230
|
+
const issued = stamp?.() ?? 0;
|
|
125
231
|
let cursor: string | undefined;
|
|
126
232
|
let result: ItemReviewView | undefined;
|
|
127
233
|
const actions: ItemReviewView['reviews']['actions'] = [];
|
|
@@ -171,6 +277,7 @@ async function fetchView(
|
|
|
171
277
|
availability: result.availability,
|
|
172
278
|
},
|
|
173
279
|
pages: refreshedPages,
|
|
280
|
+
issued,
|
|
174
281
|
};
|
|
175
282
|
}
|
|
176
283
|
/** Keep only drafts the reviewer edited; untouched ones follow the new generation. */
|
|
@@ -204,6 +311,7 @@ function syncSuggestionDrafts(current: ItemReviewView) {
|
|
|
204
311
|
current.availability === 'expired'
|
|
205
312
|
) {
|
|
206
313
|
drafts = {};
|
|
314
|
+
draftBase = {};
|
|
207
315
|
invalidDrafts = {};
|
|
208
316
|
} else if (current.availability === 'available') {
|
|
209
317
|
const attemptId = current.analysis?.attemptId;
|
|
@@ -212,11 +320,14 @@ function syncSuggestionDrafts(current: ItemReviewView) {
|
|
|
212
320
|
if (!attemptId || suggestion.disposition !== 'needs_review') return;
|
|
213
321
|
const key = `${attemptId}:${index}`;
|
|
214
322
|
keys.add(key);
|
|
215
|
-
|
|
323
|
+
const generated = JSON.stringify(suggestion.args, null, 2);
|
|
324
|
+
drafts[key] ??= generated;
|
|
325
|
+
draftBase[key] ??= generated;
|
|
216
326
|
});
|
|
217
327
|
for (const key of Object.keys(drafts))
|
|
218
328
|
if (!keys.has(key)) {
|
|
219
329
|
delete drafts[key];
|
|
330
|
+
delete draftBase[key];
|
|
220
331
|
delete invalidDrafts[key];
|
|
221
332
|
}
|
|
222
333
|
}
|
|
@@ -238,6 +349,7 @@ async function load(pageCount = loadedPages) {
|
|
|
238
349
|
if (!fetched) return;
|
|
239
350
|
if (fetched.result.availability === 'available')
|
|
240
351
|
loadedPages = fetched.pages;
|
|
352
|
+
viewIssued = fetched.issued;
|
|
241
353
|
view = fetched.result;
|
|
242
354
|
syncSuggestionDrafts(view);
|
|
243
355
|
splits = splitDrafts(view, {});
|
|
@@ -260,6 +372,7 @@ async function refresh(ticket: number) {
|
|
|
260
372
|
refreshFailed = false;
|
|
261
373
|
// An explicit host denial fails closed at once.
|
|
262
374
|
if (fetched.result.availability !== 'available') {
|
|
375
|
+
viewIssued = fetched.issued;
|
|
263
376
|
view = fetched.result;
|
|
264
377
|
syncSuggestionDrafts(view);
|
|
265
378
|
splits = {};
|
|
@@ -268,8 +381,14 @@ async function refresh(ticket: number) {
|
|
|
268
381
|
if (JSON.stringify(fetched.result) !== JSON.stringify(view)) {
|
|
269
382
|
loadedPages = fetched.pages;
|
|
270
383
|
splits = splitDrafts(fetched.result, splits, view);
|
|
384
|
+
viewIssued = fetched.issued;
|
|
271
385
|
view = fetched.result;
|
|
272
386
|
syncSuggestionDrafts(view);
|
|
387
|
+
} else if (view) {
|
|
388
|
+
// Unchanged, but read again: the displayed view is this fresh.
|
|
389
|
+
viewIssued = fetched.issued;
|
|
390
|
+
const current = view;
|
|
391
|
+
untrack(() => onview?.(current, false, viewIssued));
|
|
273
392
|
}
|
|
274
393
|
settleBusy();
|
|
275
394
|
} catch {
|
|
@@ -287,7 +406,7 @@ async function refresh(ticket: number) {
|
|
|
287
406
|
if (alive && ticket === epoch && !pending) schedulePoll(ticket);
|
|
288
407
|
}
|
|
289
408
|
}
|
|
290
|
-
async function run(operation: () => Promise<unknown
|
|
409
|
+
async function run(operation: () => Promise<unknown>, outcome?: ReviewOutcome) {
|
|
291
410
|
if (pending) return;
|
|
292
411
|
const ticket = ++epoch;
|
|
293
412
|
clearTimeout(poll);
|
|
@@ -297,7 +416,18 @@ async function run(operation: () => Promise<unknown>) {
|
|
|
297
416
|
splits = {};
|
|
298
417
|
try {
|
|
299
418
|
await operation();
|
|
300
|
-
if (alive
|
|
419
|
+
if (!alive || ticket !== epoch) return;
|
|
420
|
+
await load();
|
|
421
|
+
// Only the reload that answered this operation reports its outcome.
|
|
422
|
+
// `load` reassigned the view; TypeScript still narrows it to undefined.
|
|
423
|
+
const settled = view as ItemReviewView | undefined;
|
|
424
|
+
if (
|
|
425
|
+
outcome &&
|
|
426
|
+
alive &&
|
|
427
|
+
ticket + 1 === epoch &&
|
|
428
|
+
settled?.availability === 'available'
|
|
429
|
+
)
|
|
430
|
+
onoutcome?.(outcome, settled);
|
|
301
431
|
} catch {
|
|
302
432
|
if (alive && ticket === epoch) deny();
|
|
303
433
|
}
|
|
@@ -312,6 +442,7 @@ function offeredHandler(suggestion: GeneratedSuggestion) {
|
|
|
312
442
|
// Presentation only: the host rechecks stage, outcome, handler and authority.
|
|
313
443
|
function previewable(suggestion: GeneratedSuggestion) {
|
|
314
444
|
return (
|
|
445
|
+
!view?.entry.item.disposition &&
|
|
315
446
|
!!view?.analysis &&
|
|
316
447
|
view.generation?.outcome !== 'provider_error' &&
|
|
317
448
|
['ready_for_review', 'needs_review'].includes(suggestion.disposition)
|
|
@@ -388,6 +519,7 @@ async function preview(index: number) {
|
|
|
388
519
|
}
|
|
389
520
|
// Only a successful preview of this card consumes its draft.
|
|
390
521
|
delete drafts[key];
|
|
522
|
+
delete draftBase[key];
|
|
391
523
|
delete invalidDrafts[key];
|
|
392
524
|
await load();
|
|
393
525
|
} catch (error) {
|
|
@@ -437,6 +569,85 @@ function split(evidenceId: string) {
|
|
|
437
569
|
};
|
|
438
570
|
void run(() => host.split(input));
|
|
439
571
|
}
|
|
572
|
+
// Reviewer-chosen action and no-action disposition. Presentation only; the
|
|
573
|
+
// server previews, checks reviewer authority and requires separate approval.
|
|
574
|
+
let startHandler = $state(''),
|
|
575
|
+
startArgs = $state(''),
|
|
576
|
+
startReason = $state(''),
|
|
577
|
+
startInvalid = $state(false),
|
|
578
|
+
resolveReason = $state('');
|
|
579
|
+
const startEntry = $derived.by((): ProposalCatalogEntry | undefined =>
|
|
580
|
+
view?.catalog?.find(
|
|
581
|
+
(entry) =>
|
|
582
|
+
entry.kind === 'operation' &&
|
|
583
|
+
`${entry.id}@${entry.version}` === startHandler,
|
|
584
|
+
),
|
|
585
|
+
);
|
|
586
|
+
const startMissing = $derived.by((): string[] => {
|
|
587
|
+
if (!startEntry) return [];
|
|
588
|
+
try {
|
|
589
|
+
return missingArguments(startEntry, argumentsObject(startArgs));
|
|
590
|
+
} catch {
|
|
591
|
+
return [];
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
function draftStart() {
|
|
595
|
+
startInvalid = false;
|
|
596
|
+
const entry = view?.catalog?.find(
|
|
597
|
+
(candidate) => `${candidate.id}@${candidate.version}` === startHandler,
|
|
598
|
+
);
|
|
599
|
+
startArgs = entry
|
|
600
|
+
? JSON.stringify(
|
|
601
|
+
draftArguments(
|
|
602
|
+
entry,
|
|
603
|
+
view?.generation?.suggestions ?? [],
|
|
604
|
+
view?.evidence.map((part) => part.evidence.id) ?? [],
|
|
605
|
+
),
|
|
606
|
+
null,
|
|
607
|
+
2,
|
|
608
|
+
)
|
|
609
|
+
: '';
|
|
610
|
+
}
|
|
611
|
+
function startAction() {
|
|
612
|
+
const entry = startEntry;
|
|
613
|
+
if (!entry || !view?.analysis || !host.startAction) return;
|
|
614
|
+
let args: Record<string, unknown>;
|
|
615
|
+
try {
|
|
616
|
+
args = argumentsObject(startArgs);
|
|
617
|
+
} catch {
|
|
618
|
+
startInvalid = true;
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const input = {
|
|
622
|
+
itemId,
|
|
623
|
+
handlerId: entry.id,
|
|
624
|
+
handlerVersion: entry.version,
|
|
625
|
+
args,
|
|
626
|
+
requestId: crypto.randomUUID(),
|
|
627
|
+
...(startReason ? { reason: startReason } : {}),
|
|
628
|
+
};
|
|
629
|
+
const start = host.startAction;
|
|
630
|
+
void run(async () => {
|
|
631
|
+
await start(input);
|
|
632
|
+
startHandler = '';
|
|
633
|
+
startArgs = '';
|
|
634
|
+
startReason = '';
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
function resolve() {
|
|
638
|
+
if (!host.resolve) return;
|
|
639
|
+
const input = {
|
|
640
|
+
itemId,
|
|
641
|
+
disposition: 'resolve_no_action' as const,
|
|
642
|
+
requestId: crypto.randomUUID(),
|
|
643
|
+
...(resolveReason ? { reason: resolveReason } : {}),
|
|
644
|
+
};
|
|
645
|
+
const close = host.resolve;
|
|
646
|
+
void run(async () => {
|
|
647
|
+
await close(input);
|
|
648
|
+
resolveReason = '';
|
|
649
|
+
}, 'resolve');
|
|
650
|
+
}
|
|
440
651
|
function instruct() {
|
|
441
652
|
const binding = instructionBinding(view);
|
|
442
653
|
const text = instruction.trim();
|
|
@@ -523,7 +734,7 @@ onMount(() => {
|
|
|
523
734
|
{:else}
|
|
524
735
|
{@const extracted = view.analysis ? extractedContent(view.analysis.result.output) : []}
|
|
525
736
|
<h2>{view.entry.label}</h2><p>{t(M[`ingestion.${view.entry.state}`])}</p>
|
|
526
|
-
<div class="review-grid"><EvidenceViewer evidence={view.evidence} /><div class="details">
|
|
737
|
+
<div class="review-grid" class:stacked={layout === 'stacked'}><EvidenceViewer evidence={view.evidence} pages={observedPages(view)} layout={layout === 'stacked' ? 'pager' : 'stack'} bind:chosen={viewerPart} bind:page={viewerPages} /><div class="details">
|
|
527
738
|
{#if view.instruction}
|
|
528
739
|
{@const applied = view.instruction.applied}{@const current = view.instruction.current}
|
|
529
740
|
{#if inFlight(view) && current}<p role="status" data-testid="reinterpreting">{t(M['ingestion.reinterpreting'])}</p>{/if}
|
|
@@ -533,7 +744,7 @@ onMount(() => {
|
|
|
533
744
|
{#if view.instruction.dropped}<p role="status" data-testid="dropped-instruction">{t(M['ingestion.droppedInstruction'])}: <q>{view.instruction.dropped.text}</q></p>{/if}
|
|
534
745
|
{/if}
|
|
535
746
|
{#if instructionBusy}<p role="status" data-testid="instruction-busy">{t(M['ingestion.instructionBusy'])}</p>{:else if unrecordedDraft !== undefined && unrecordedDraft === instruction}<p role="status" data-testid="instruction-not-recorded">{t(M['ingestion.instructionNotRecorded'])}</p>{/if}
|
|
536
|
-
{#if host.instruct && instructionBinding(view)}
|
|
747
|
+
{#if host.instruct && !view.entry.item.disposition && instructionBinding(view)}
|
|
537
748
|
<div><Textarea aria-label={t(M['ingestion.instructions'])} aria-describedby="instruction-length" maxlength={INSTRUCTION_MAX_LENGTH} bind:value={instruction} /><p id="instruction-length" data-testid="instruction-length">{t(M['ingestion.instructionLength'], { count: instruction.length, max: INSTRUCTION_MAX_LENGTH })}</p>{#if invalidDraft !== undefined && invalidDraft === instruction}<p role="alert" data-testid="instruction-invalid">{t(M['ingestion.instructionInvalid'], { max: INSTRUCTION_MAX_LENGTH })}</p>{/if}<Button disabled={!instructionValid || inFlight(view)} onclick={instruct}>{t(M['ingestion.reinterpret'])}</Button></div>
|
|
538
749
|
{/if}
|
|
539
750
|
{#if extracted.length}
|
|
@@ -555,6 +766,7 @@ onMount(() => {
|
|
|
555
766
|
{#if suggestion.alternatives.length}<h4>{t(M['ingestion.alternatives'])}</h4><ul>{#each suggestion.alternatives as alternative}<li>{alternative}</li>{/each}</ul>{/if}
|
|
556
767
|
{#if suggestion.missingFields.length}<h4>{t(M['ingestion.missing'])}</h4><ul>{#each suggestion.missingFields as missing}<li>{missing}</li>{/each}</ul>{/if}
|
|
557
768
|
<ul>{#each suggestion.evidence as reference}{@const url = locationUrl(reference.evidenceId,reference.location)}<li>{#if url}<Button href={url} target="_blank" rel="noopener noreferrer">{locationLabel(reference.location)}</Button>{:else}{locationLabel(reference.location)}{/if}</li>{/each}</ul>
|
|
769
|
+
{#if !view.entry.item.disposition}
|
|
558
770
|
{#if suggestion.disposition === 'needs_review' && previewable(suggestion)}
|
|
559
771
|
{@const handler = offeredHandler(suggestion)}
|
|
560
772
|
{@const key = draftKey(index)}
|
|
@@ -566,14 +778,43 @@ onMount(() => {
|
|
|
566
778
|
{/if}
|
|
567
779
|
{#if previewRejected === index}<p role="alert">{t(M['ingestion.previewRejected'])}</p>{/if}
|
|
568
780
|
<Button disabled={!canPreview(index)} onclick={() => preview(index)}>{t(M['ingestion.preview'])}</Button>
|
|
781
|
+
{/if}
|
|
569
782
|
</article>
|
|
570
783
|
{/each}
|
|
571
|
-
{#each generation.splits as proposed (proposed.evidenceId)}<div><Textarea aria-label={`${t(M['ingestion.split'])}: ${proposed.evidenceId}`} bind:value={splits[proposed.evidenceId]} /><Button disabled={!view.analysis} onclick={() => split(proposed.evidenceId)}>{t(M['ingestion.saveSplit'])}</Button></div>{/each}
|
|
784
|
+
{#if !view.entry.item.disposition}{#each generation.splits as proposed (proposed.evidenceId)}<div><Textarea aria-label={`${t(M['ingestion.split'])}: ${proposed.evidenceId}`} bind:value={splits[proposed.evidenceId]} /><Button disabled={!view.analysis} onclick={() => split(proposed.evidenceId)}>{t(M['ingestion.saveSplit'])}</Button></div>{/each}{/if}
|
|
572
785
|
{#if invalid}<p role="alert">{t(M['ingestion.invalid'])}</p>{/if}
|
|
573
786
|
{/if}
|
|
787
|
+
{#if view.entry.item.disposition}
|
|
788
|
+
{#if view.disposition?.reviewer}
|
|
789
|
+
<p data-testid="disposition">{t(M['ingestion.resolvedBy'], { reviewer: view.disposition.reviewer })}{#if view.disposition.reason}: {view.disposition.reason}{/if}</p>
|
|
790
|
+
{:else}
|
|
791
|
+
<p data-testid="disposition">{t(M['ingestion.resolved'])}</p>
|
|
792
|
+
{/if}
|
|
793
|
+
{:else}
|
|
794
|
+
{#if host.startAction && view.catalog?.some((entry) => entry.kind === 'operation')}
|
|
795
|
+
<fieldset class="start"><legend>{t(M['ingestion.startAs'])}</legend>
|
|
796
|
+
<Select aria-label={t(M['ingestion.startHandler'])} bind:value={startHandler} onchange={draftStart} disabled={pending}><option value="">{t(M['ingestion.startHandler'])}</option>{#each view.catalog as entry (`${entry.id}@${entry.version}`)}{#if entry.kind === 'operation'}<option value={`${entry.id}@${entry.version}`}>{entry.id}</option>{/if}{/each}</Select>
|
|
797
|
+
{#if startEntry}
|
|
798
|
+
<p>{startEntry.description}</p>
|
|
799
|
+
<Textarea aria-label={t(M['ingestion.startArgs'])} bind:value={startArgs} rows={8} />
|
|
800
|
+
{#if startMissing.length}<h4>{t(M['ingestion.missing'])}</h4><ul>{#each startMissing as missing}<li>{missing}</li>{/each}</ul>{/if}
|
|
801
|
+
<Input aria-label={t(M['ingestion.reason'])} bind:value={startReason} />
|
|
802
|
+
<p>{t(M['ingestion.startHelp'])}</p>
|
|
803
|
+
{#if startInvalid}<p role="alert">{t(M['ingestion.invalid'])}</p>{/if}
|
|
804
|
+
<Button disabled={pending || !view.analysis} onclick={startAction}>{t(M['ingestion.start'])}</Button>
|
|
805
|
+
{/if}
|
|
806
|
+
</fieldset>
|
|
807
|
+
{/if}
|
|
808
|
+
{#if host.resolve}
|
|
809
|
+
<fieldset class="resolve"><legend>{t(M['ingestion.resolveNoAction'])}</legend>
|
|
810
|
+
<Input aria-label={t(M['ingestion.resolveReason'])} bind:value={resolveReason} disabled={pending} />
|
|
811
|
+
<Button disabled={pending} onclick={resolve}>{t(M['ingestion.resolveNoAction'])}</Button>
|
|
812
|
+
</fieldset>
|
|
813
|
+
{/if}
|
|
814
|
+
{/if}
|
|
574
815
|
{#if !view.reviews.actions.length}<p>{t(M['ingestion.noReviews'])}</p>{/if}
|
|
575
816
|
{#each view.reviews.actions as action (`${action.review.actionId}:${action.review.revision}:${action.review.reviewVersion}`)}
|
|
576
|
-
<ActionReview {host} {itemId} {action} {run} {deny} currentAttemptId={action.stalePlan && view.reviews.actions.find((entry) => entry.stalePlan?.id === action.stalePlan?.id) === action ? action.attemptId : undefined} catalog={view.generation?.offered.find((entry) => entry.handler.id === action.handlerId && entry.handler.version === action.handlerVersion)?.handler} />
|
|
817
|
+
<ActionReview {host} {itemId} {action} {run} {deny} ondirty={(edited) => { actionDirty[action.review.actionId] = edited; }} resolved={!!view.entry.item.disposition} currentAttemptId={action.stalePlan && view.reviews.actions.find((entry) => entry.stalePlan?.id === action.stalePlan?.id) === action ? action.attemptId : undefined} catalog={view.generation?.offered.find((entry) => entry.handler.id === action.handlerId && entry.handler.version === action.handlerVersion)?.handler} />
|
|
577
818
|
{/each}
|
|
578
819
|
{#if view.reviews.nextCursor}<Button onclick={() => load(loadedPages + 1)}>{t(M['ingestion.more'])}</Button>{/if}
|
|
579
820
|
</div></div>
|
|
@@ -582,6 +823,6 @@ onMount(() => {
|
|
|
582
823
|
<style>
|
|
583
824
|
@layer smrt.reset, smrt.structure, smrt.theme;
|
|
584
825
|
@layer smrt.theme {
|
|
585
|
-
section{overflow-wrap:anywhere;max-width:90rem;margin-inline:auto;padding:1rem;color:var(--smrt-color-on-surface)}header{display:flex;align-items:center;justify-content:space-between;gap:1rem}.review-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:1.5rem}.details,article{min-width:0}pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:24rem;overflow:auto}article{padding-block:1rem;border-block-end:var(--smrt-hairline)}@media(max-width:48rem){.review-grid{grid-template-columns:minmax(0,1fr)}header{flex-wrap:wrap}}
|
|
826
|
+
section{overflow-wrap:anywhere;max-width:90rem;margin-inline:auto;padding:1rem;color:var(--smrt-color-on-surface)}header{display:flex;align-items:center;justify-content:space-between;gap:1rem}.review-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:1.5rem}.review-grid.stacked{grid-template-columns:minmax(0,1fr)}.details,article{min-width:0}pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:24rem;overflow:auto}article{padding-block:1rem;border-block-end:var(--smrt-hairline)}fieldset{margin-block:1rem;min-width:0;display:grid;gap:.5rem}@media(max-width:48rem){.review-grid{grid-template-columns:minmax(0,1fr)}header{flex-wrap:wrap}}
|
|
586
827
|
}
|
|
587
828
|
</style>
|
|
@@ -1,9 +1,26 @@
|
|
|
1
|
-
import type { IntakeReviewHost } from '../../review-dto.js';
|
|
1
|
+
import type { IntakeReviewHost, ItemReviewView } from '../../review-dto.js';
|
|
2
|
+
import { type ReviewOutcome } from './ActionReview.svelte';
|
|
2
3
|
export interface Props {
|
|
3
4
|
host: IntakeReviewHost;
|
|
4
5
|
itemId: string;
|
|
5
6
|
/** Reload interval while the host reports the item as processing. */
|
|
6
7
|
pollMs?: number;
|
|
8
|
+
/** `split` puts the original beside the form; `stacked` puts it above, one part at a time. */
|
|
9
|
+
layout?: 'split' | 'stacked';
|
|
10
|
+
/**
|
|
11
|
+
* Every displayed view (load, in-place poll patch, denial projection), and
|
|
12
|
+
* `undefined` when a displayed view is cleared for a reload or an operation
|
|
13
|
+
* whose result has not been shown yet. `denied` is true when it was cleared
|
|
14
|
+
* because a callback failed (the view fails closed), not for a reload.
|
|
15
|
+
* Presentation only.
|
|
16
|
+
*/
|
|
17
|
+
onview?: (view: ItemReviewView | undefined, denied: boolean, issued: number) => void;
|
|
18
|
+
/** Numbers each load when its request starts; reported as `issued` with the view. */
|
|
19
|
+
stamp?: () => number;
|
|
20
|
+
/** Whether the reviewer has unsent input on this item. */
|
|
21
|
+
ondirty?: (dirty: boolean) => void;
|
|
22
|
+
/** After an approve, apply or resolve and the reload that follows it succeeded. */
|
|
23
|
+
onoutcome?: (outcome: ReviewOutcome, view: ItemReviewView) => void;
|
|
7
24
|
}
|
|
8
25
|
declare const ReviewView: import("svelte").Component<Props, {}, "">;
|
|
9
26
|
type ReviewView = ReturnType<typeof ReviewView>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReviewView.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ReviewView.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ReviewView.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ReviewView.svelte.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAa5E,OAAqB,EAAE,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAIzE,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8FAA8F;IAC9F,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,CACP,IAAI,EAAE,cAAc,GAAG,SAAS,EAChC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,KACX,IAAI,CAAC;IACV,qFAAqF;IACrF,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IACrB,0DAA0D;IAC1D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,mFAAmF;IACnF,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;CACpE;AAqxBD,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|