@happyvertical/smrt-ingestion 0.57.21 → 0.57.23
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 +77 -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/proposal-sdk.d.ts +54 -1
- package/dist/proposal-sdk.d.ts.map +1 -1
- package/dist/proposals.d.ts +2 -1
- package/dist/proposals.d.ts.map +1 -1
- package/dist/proposals.js +215 -10
- package/dist/proposals.js.map +1 -1
- package/dist/provider-failure.d.ts +3 -0
- package/dist/provider-failure.d.ts.map +1 -0
- 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 +128 -22
- 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,3 +1,7 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/** A reviewer operation whose result may finish the item for this reviewer. */
|
|
3
|
+
export type ReviewOutcome = 'approve' | 'apply' | 'resolve';
|
|
4
|
+
</script>
|
|
1
5
|
<script lang="ts">
|
|
2
6
|
import { Input, Textarea } from '@happyvertical/smrt-ui/forms';
|
|
3
7
|
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
@@ -15,11 +19,28 @@ export interface Props {
|
|
|
15
19
|
action: ReviewAction;
|
|
16
20
|
catalog?: ProposalCatalogEntry;
|
|
17
21
|
currentAttemptId?: string;
|
|
18
|
-
|
|
22
|
+
/** A resolved item is terminal: approve/correct/defer/apply and edits are hidden; only reject remains. */
|
|
23
|
+
resolved?: boolean;
|
|
24
|
+
/** `outcome` names an approve or apply so the parent can report it after its reload. */
|
|
25
|
+
run: (
|
|
26
|
+
operation: () => Promise<unknown>,
|
|
27
|
+
outcome?: ReviewOutcome,
|
|
28
|
+
) => Promise<void>;
|
|
19
29
|
deny: () => void;
|
|
30
|
+
/** Whether this editor holds unsent input (arguments, reason, comment). */
|
|
31
|
+
ondirty?: (dirty: boolean) => void;
|
|
20
32
|
}
|
|
21
|
-
let {
|
|
22
|
-
|
|
33
|
+
let {
|
|
34
|
+
host,
|
|
35
|
+
itemId,
|
|
36
|
+
action,
|
|
37
|
+
catalog,
|
|
38
|
+
currentAttemptId,
|
|
39
|
+
resolved = false,
|
|
40
|
+
run,
|
|
41
|
+
deny,
|
|
42
|
+
ondirty,
|
|
43
|
+
}: Props = $props();
|
|
23
44
|
const { t } = useI18n();
|
|
24
45
|
// ReviewView keys this editor by action/revision/reviewVersion; edits are local to that binding.
|
|
25
46
|
let args = $state(untrack(() => JSON.stringify(action.args, null, 2) ?? '')),
|
|
@@ -32,11 +53,18 @@ let args = $state(untrack(() => JSON.stringify(action.args, null, 2) ?? '')),
|
|
|
32
53
|
let alive = true;
|
|
33
54
|
onDestroy(() => {
|
|
34
55
|
alive = false;
|
|
56
|
+
// The editor's local input goes with it.
|
|
57
|
+
ondirty?.(false);
|
|
35
58
|
});
|
|
36
59
|
const dirty = $derived(
|
|
37
60
|
args !== (JSON.stringify(action.args, null, 2) ?? '') ||
|
|
38
61
|
planArgs !== (JSON.stringify(action.plan?.args, null, 2) ?? ''),
|
|
39
62
|
);
|
|
63
|
+
const unsent = $derived(dirty || !!reason || !!comment);
|
|
64
|
+
$effect(() => {
|
|
65
|
+
const edited = unsent;
|
|
66
|
+
untrack(() => ondirty?.(edited));
|
|
67
|
+
});
|
|
40
68
|
const waiting = $derived(
|
|
41
69
|
action.args !== undefined && action.review.state === 'waiting_review',
|
|
42
70
|
);
|
|
@@ -87,6 +115,7 @@ async function decide(decision: ReviewInput['decision']) {
|
|
|
87
115
|
...(reason ? { reason } : {}),
|
|
88
116
|
...(correctedArgs ? { correctedArgs } : {}),
|
|
89
117
|
}),
|
|
118
|
+
decision === 'approve' ? 'approve' : undefined,
|
|
90
119
|
);
|
|
91
120
|
}
|
|
92
121
|
async function saveEdits() {
|
|
@@ -161,6 +190,7 @@ function choose(field: string, id: string) {
|
|
|
161
190
|
</script>
|
|
162
191
|
<article data-testid={`action-${action.review.actionId}`}>
|
|
163
192
|
<h3>{action.handlerId ?? action.review.actionId}</h3>
|
|
193
|
+
{#if action.origin === 'reviewer-selected'}<p>{t(M['ingestion.reviewerSelected'])}</p>{/if}
|
|
164
194
|
<p>{t(M['ingestion.state'])}: {action.review.state}</p>
|
|
165
195
|
{#if action.args !== undefined}
|
|
166
196
|
<h4>{t(M['ingestion.effects'])}</h4><pre>{JSON.stringify(action.review.display,null,2)}</pre>
|
|
@@ -171,30 +201,30 @@ function choose(field: string, id: string) {
|
|
|
171
201
|
{#if !action.plan && catalog}<CandidatePicker {host} {itemId} {catalog} {deny} onchoose={choose} />{/if}
|
|
172
202
|
{#if dirty}<p>{t(M['ingestion.reviewAgain'])}</p>{/if}
|
|
173
203
|
{#if invalid}<p role="alert">{t(M['ingestion.invalid'])}</p>{/if}
|
|
174
|
-
<div class="controls">{#if waiting}<Button disabled={dirty} onclick={() => decide('approve')}>{t(M['ingestion.approve'])}</Button>{/if}{#if !action.plan}<Button disabled={!dirty} onclick={saveEdits}>{t(M['ingestion.correct'])}</Button>{/if}{#if waiting}<Button onclick={() => decide('reject')}>{t(M['ingestion.reject'])}</Button
|
|
204
|
+
<div class="controls">{#if waiting && !resolved}<Button disabled={dirty} onclick={() => decide('approve')}>{t(M['ingestion.approve'])}</Button>{/if}{#if !action.plan && !resolved}<Button disabled={!dirty} onclick={saveEdits}>{t(M['ingestion.correct'])}</Button>{/if}{#if waiting}<Button onclick={() => decide('reject')}>{t(M['ingestion.reject'])}</Button>{#if !resolved}<Button onclick={() => decide('defer')}>{t(M['ingestion.defer'])}</Button>{/if}{/if}</div>
|
|
175
205
|
{/if}
|
|
176
|
-
{#if action.plan?.stepIndex === 0 && action.plan.args && host.editPlan && planEditable}
|
|
206
|
+
{#if action.plan?.stepIndex === 0 && action.plan.args && host.editPlan && planEditable && !resolved}
|
|
177
207
|
<Textarea aria-label={t(M['ingestion.planArgs'])} bind:value={planArgs} rows={8} />
|
|
178
208
|
{#if !editable && dirty}<p>{t(M['ingestion.reviewAgain'])}</p>{/if}
|
|
179
209
|
{#if !editable && invalid}<p role="alert">{t(M['ingestion.invalid'])}</p>{/if}
|
|
180
210
|
<Button onclick={editPlan}>{t(M['ingestion.editPlan'])}</Button>
|
|
181
211
|
{/if}
|
|
182
|
-
{#if action.review.state === 'authorized'}<Button disabled={
|
|
212
|
+
{#if action.review.state === 'authorized' && !resolved}<Button disabled={unsent} onclick={() => { if (!unsent) void run(() => host.apply(action.review.actionId), 'apply'); }}>{t(M['ingestion.apply'])}</Button>{/if}
|
|
183
213
|
{/if}
|
|
184
|
-
{#if action.review.state === 'stale' && !action.stalePlan && action.attemptId && host.editAction}
|
|
214
|
+
{#if action.review.state === 'stale' && !action.stalePlan && action.attemptId && host.editAction && !resolved}
|
|
185
215
|
<p>{t(M['ingestion.reviewAgain'])}</p>
|
|
186
216
|
<Textarea aria-label={t(M['ingestion.args'])} bind:value={args} rows={8} />
|
|
187
217
|
{#if invalid}<p role="alert">{t(M['ingestion.invalid'])}</p>{/if}
|
|
188
218
|
<Button onclick={saveEdits}>{t(M['ingestion.correct'])}</Button>
|
|
189
219
|
{/if}
|
|
190
|
-
{#if action.review.state === 'stale' && action.stalePlan && currentAttemptId && host.editPlan}
|
|
220
|
+
{#if action.review.state === 'stale' && action.stalePlan && currentAttemptId && host.editPlan && !resolved}
|
|
191
221
|
<p>{t(M['ingestion.reviewAgain'])}</p>
|
|
192
222
|
<Textarea aria-label={t(M['ingestion.planArgs'])} bind:value={planArgs} rows={8} />
|
|
193
223
|
{#if invalid}<p role="alert">{t(M['ingestion.invalid'])}</p>{/if}
|
|
194
224
|
<Button onclick={editPlan}>{t(M['ingestion.editPlan'])}</Button>
|
|
195
225
|
{/if}
|
|
196
226
|
{#if action.result}<h4>{t(M['ingestion.result'])}</h4><pre>{JSON.stringify(action.result,null,2)}</pre>{/if}
|
|
197
|
-
{#if host.feedback && action.args !== undefined}<fieldset><legend>{t(M['ingestion.feedback'])}</legend><Textarea aria-label={t(M['ingestion.comment'])} bind:value={comment} /><Button disabled={dirty} onclick={() => run(() => host.feedback?.({itemId,actionId:action.review.actionId,expectedRevision:action.review.revision,expectedReviewVersion:action.review.reviewVersion,bindingHash:action.review.bindingHash,judgment:'correct',comment,requestId:crypto.randomUUID()}) ?? Promise.resolve())}>{t(M['ingestion.correctJudgment'])}</Button><Button disabled={dirty} onclick={() => run(() => host.feedback?.({itemId,actionId:action.review.actionId,expectedRevision:action.review.revision,expectedReviewVersion:action.review.reviewVersion,bindingHash:action.review.bindingHash,judgment:'incorrect',comment,requestId:crypto.randomUUID()}) ?? Promise.resolve())}>{t(M['ingestion.incorrectJudgment'])}</Button></fieldset>{/if}
|
|
227
|
+
{#if host.feedback && action.args !== undefined && action.origin !== 'reviewer-selected'}<fieldset><legend>{t(M['ingestion.feedback'])}</legend><Textarea aria-label={t(M['ingestion.comment'])} bind:value={comment} /><Button disabled={dirty} onclick={() => run(() => host.feedback?.({itemId,actionId:action.review.actionId,expectedRevision:action.review.revision,expectedReviewVersion:action.review.reviewVersion,bindingHash:action.review.bindingHash,judgment:'correct',comment,requestId:crypto.randomUUID()}) ?? Promise.resolve())}>{t(M['ingestion.correctJudgment'])}</Button><Button disabled={dirty} onclick={() => run(() => host.feedback?.({itemId,actionId:action.review.actionId,expectedRevision:action.review.revision,expectedReviewVersion:action.review.reviewVersion,bindingHash:action.review.bindingHash,judgment:'incorrect',comment,requestId:crypto.randomUUID()}) ?? Promise.resolve())}>{t(M['ingestion.incorrectJudgment'])}</Button></fieldset>{/if}
|
|
198
228
|
</article>
|
|
199
229
|
<style>
|
|
200
230
|
@layer smrt.reset, smrt.structure, smrt.theme;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** A reviewer operation whose result may finish the item for this reviewer. */
|
|
2
|
+
export type ReviewOutcome = 'approve' | 'apply' | 'resolve';
|
|
1
3
|
import type { ProposalCatalogEntry } from '../../proposal-dto.js';
|
|
2
4
|
import type { IntakeReviewHost, ReviewAction } from '../../review-dto.js';
|
|
3
5
|
export interface Props {
|
|
@@ -6,8 +8,13 @@ export interface Props {
|
|
|
6
8
|
action: ReviewAction;
|
|
7
9
|
catalog?: ProposalCatalogEntry;
|
|
8
10
|
currentAttemptId?: string;
|
|
9
|
-
|
|
11
|
+
/** A resolved item is terminal: approve/correct/defer/apply and edits are hidden; only reject remains. */
|
|
12
|
+
resolved?: boolean;
|
|
13
|
+
/** `outcome` names an approve or apply so the parent can report it after its reload. */
|
|
14
|
+
run: (operation: () => Promise<unknown>, outcome?: ReviewOutcome) => Promise<void>;
|
|
10
15
|
deny: () => void;
|
|
16
|
+
/** Whether this editor holds unsent input (arguments, reason, comment). */
|
|
17
|
+
ondirty?: (dirty: boolean) => void;
|
|
11
18
|
}
|
|
12
19
|
declare const ActionReview: import("svelte").Component<Props, {}, "">;
|
|
13
20
|
type ActionReview = ReturnType<typeof ActionReview>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActionReview.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ActionReview.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ActionReview.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ActionReview.svelte.ts"],"names":[],"mappings":"AAGA,+EAA+E;AAC/E,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;AAQ5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAK1E,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,0GAA0G;IAC1G,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wFAAwF;IACxF,GAAG,EAAE,CACH,SAAS,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,EACjC,OAAO,CAAC,EAAE,aAAa,KACpB,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAsND,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -6,19 +6,75 @@ import { M } from '../i18n.js';
|
|
|
6
6
|
import { evidenceUrl } from '../view-utils.js';
|
|
7
7
|
export interface Props {
|
|
8
8
|
evidence: EvidenceView[];
|
|
9
|
+
/**
|
|
10
|
+
* Highest observed page per evidence id. A PDF with more than one observed
|
|
11
|
+
* page gets inline page navigation (the same-origin URL plus `#page=n`).
|
|
12
|
+
*/
|
|
13
|
+
pages?: Record<string, number>;
|
|
14
|
+
/** `stack` lists every part; `pager` shows one part at a time with navigation. */
|
|
15
|
+
layout?: 'stack' | 'pager';
|
|
16
|
+
/** Bindable: the part the pager shows, by evidence id. */
|
|
17
|
+
chosen?: string;
|
|
18
|
+
/** Bindable: page per evidence id; absent until the reviewer navigates. */
|
|
19
|
+
page?: Record<string, number>;
|
|
9
20
|
}
|
|
10
|
-
let {
|
|
21
|
+
let {
|
|
22
|
+
evidence,
|
|
23
|
+
pages = {},
|
|
24
|
+
layout = 'stack',
|
|
25
|
+
// Bindable so a parent can keep them while this viewer remounts on reload.
|
|
26
|
+
chosen = $bindable(),
|
|
27
|
+
page = $bindable({}),
|
|
28
|
+
}: Props = $props();
|
|
11
29
|
const { t } = useI18n();
|
|
30
|
+
const inline = (view: EvidenceView) =>
|
|
31
|
+
view.evidence.mediaType.startsWith('image/') ||
|
|
32
|
+
view.evidence.mediaType === 'application/pdf' ||
|
|
33
|
+
view.evidence.mediaType.startsWith('audio/');
|
|
34
|
+
// The pager opens on the first part that renders inline (the uploaded file,
|
|
35
|
+
// not its capture record); the reviewer can step to every other part.
|
|
36
|
+
const shown = $derived.by(() => {
|
|
37
|
+
if (layout !== 'pager') return evidence;
|
|
38
|
+
const current =
|
|
39
|
+
evidence.find((view) => view.evidence.id === chosen) ??
|
|
40
|
+
evidence.find((view) => inline(view) && evidenceUrl(view.viewUrl)) ??
|
|
41
|
+
evidence[0];
|
|
42
|
+
return current ? [current] : [];
|
|
43
|
+
});
|
|
44
|
+
const position = $derived(shown.length === 1 ? evidence.indexOf(shown[0]) : -1);
|
|
45
|
+
function step(offset: number) {
|
|
46
|
+
const next = evidence[position + offset];
|
|
47
|
+
if (next) chosen = next.evidence.id;
|
|
48
|
+
}
|
|
49
|
+
function pageUrl(url: string, id: string) {
|
|
50
|
+
const current = page[id];
|
|
51
|
+
return current === undefined ? url : `${url.split('#')[0]}#page=${current}`;
|
|
52
|
+
}
|
|
53
|
+
function turn(id: string, offset: number) {
|
|
54
|
+
const count = pages[id] ?? 1;
|
|
55
|
+
page = {
|
|
56
|
+
...page,
|
|
57
|
+
[id]: Math.min(count, Math.max(1, (page[id] ?? 1) + offset)),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
12
60
|
</script>
|
|
13
61
|
<section data-testid="evidence-viewer" aria-label={t(M['ingestion.evidence'])} data-smrt-component="EvidenceViewer">
|
|
14
62
|
<h2>{t(M['ingestion.evidence'])}</h2>
|
|
15
|
-
{#
|
|
63
|
+
{#if layout === 'pager' && evidence.length > 1 && position >= 0}
|
|
64
|
+
<div class="pager" data-testid="evidence-pager"><Button disabled={position <= 0} onclick={() => step(-1)}>{t(M['ingestion.previousOriginal'])}</Button><p role="status">{t(M['ingestion.originalOf'], { index: position + 1, count: evidence.length })}</p><Button disabled={position >= evidence.length - 1} onclick={() => step(1)}>{t(M['ingestion.nextOriginal'])}</Button></div>
|
|
65
|
+
{/if}
|
|
66
|
+
{#each shown as view (view.evidence.id)}
|
|
16
67
|
{@const url = evidenceUrl(view.viewUrl)}
|
|
68
|
+
{@const count = pages[view.evidence.id] ?? 0}
|
|
17
69
|
<article id={`evidence-${view.evidence.id}`}>
|
|
18
70
|
<h3>{view.label}</h3>
|
|
19
71
|
{#if url}
|
|
20
72
|
{#if view.evidence.mediaType.startsWith('image/')}<img src={url} alt={view.label} />
|
|
21
|
-
{:else if view.evidence.mediaType === 'application/pdf'}
|
|
73
|
+
{:else if view.evidence.mediaType === 'application/pdf'}
|
|
74
|
+
{@const src = pageUrl(url, view.evidence.id)}
|
|
75
|
+
{#if count > 1}{@const current = page[view.evidence.id] ?? 1}<div class="pager" data-testid="page-pager"><Button disabled={current <= 1} onclick={() => turn(view.evidence.id, -1)}>{t(M['ingestion.previousPage'])}</Button><p role="status">{t(M['ingestion.pageOf'], { page: current, count })}</p><Button disabled={current >= count} onclick={() => turn(view.evidence.id, 1)}>{t(M['ingestion.nextPage'])}</Button></div>{/if}
|
|
76
|
+
<!-- A fragment-only change does not reliably move an embedded PDF viewer; a new frame does. -->
|
|
77
|
+
{#key src}<iframe {src} title={view.label}></iframe>{/key}
|
|
22
78
|
{:else if view.evidence.mediaType.startsWith('audio/')}<audio controls src={url} aria-label={view.label}></audio>{/if}
|
|
23
79
|
<Button href={url} target="_blank" rel="noopener noreferrer">{t(M['ingestion.original'])}</Button>
|
|
24
80
|
{:else}<p>{t(M['ingestion.noOriginal'])}</p>{/if}
|
|
@@ -29,6 +85,6 @@ const { t } = useI18n();
|
|
|
29
85
|
<style>
|
|
30
86
|
@layer smrt.reset, smrt.structure, smrt.theme;
|
|
31
87
|
@layer smrt.theme {
|
|
32
|
-
section,article{min-width:0}article{margin-block:1rem}img,iframe,audio{display:block;max-width:100%;width:100%;margin-block:.5rem}img{object-fit:contain;max-height:32rem}iframe{height:32rem;border:var(--smrt-hairline)}pre{white-space:pre-wrap;overflow-wrap:anywhere}
|
|
88
|
+
section,article{min-width:0}article{margin-block:1rem}img,iframe,audio{display:block;max-width:100%;width:100%;margin-block:.5rem}img{object-fit:contain;max-height:32rem}iframe{height:32rem;border:var(--smrt-hairline)}pre{white-space:pre-wrap;overflow-wrap:anywhere}.pager{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.pager p{margin:0}
|
|
33
89
|
}
|
|
34
90
|
</style>
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import type { EvidenceView } from '../../review-dto.js';
|
|
2
2
|
export interface Props {
|
|
3
3
|
evidence: EvidenceView[];
|
|
4
|
+
/**
|
|
5
|
+
* Highest observed page per evidence id. A PDF with more than one observed
|
|
6
|
+
* page gets inline page navigation (the same-origin URL plus `#page=n`).
|
|
7
|
+
*/
|
|
8
|
+
pages?: Record<string, number>;
|
|
9
|
+
/** `stack` lists every part; `pager` shows one part at a time with navigation. */
|
|
10
|
+
layout?: 'stack' | 'pager';
|
|
11
|
+
/** Bindable: the part the pager shows, by evidence id. */
|
|
12
|
+
chosen?: string;
|
|
13
|
+
/** Bindable: page per evidence id; absent until the reviewer navigates. */
|
|
14
|
+
page?: Record<string, number>;
|
|
4
15
|
}
|
|
5
|
-
declare const EvidenceViewer: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
declare const EvidenceViewer: import("svelte").Component<Props, {}, "page" | "chosen">;
|
|
6
17
|
type EvidenceViewer = ReturnType<typeof EvidenceViewer>;
|
|
7
18
|
export default EvidenceViewer;
|
|
8
19
|
//# sourceMappingURL=EvidenceViewer.svelte.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EvidenceViewer.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/EvidenceViewer.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAIxD,MAAM,WAAW,KAAK;IACpB,QAAQ,EAAE,YAAY,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"EvidenceViewer.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/EvidenceViewer.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAIxD,MAAM,WAAW,KAAK;IACpB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,kFAAkF;IAClF,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAC3B,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AA4ED,QAAA,MAAM,cAAc,0DAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -19,8 +19,39 @@ export interface Props {
|
|
|
19
19
|
* page of rows does not add one complementary landmark per row.
|
|
20
20
|
*/
|
|
21
21
|
rowAside?: Snippet<[InboxEntry]>;
|
|
22
|
+
/** The row a workspace has open; marked `aria-current`. */
|
|
23
|
+
selectedId?: string;
|
|
24
|
+
/**
|
|
25
|
+
* The displayed rows after every settled load or page append (not after
|
|
26
|
+
* `patch`, which returns them), whether more pages exist, and the entries
|
|
27
|
+
* exactly as the page read returned them (`appended` for Load more), absent
|
|
28
|
+
* when the read failed.
|
|
29
|
+
*/
|
|
30
|
+
onitems?: (
|
|
31
|
+
items: InboxEntry[],
|
|
32
|
+
more: boolean,
|
|
33
|
+
page?: { entries: InboxEntry[]; appended: boolean; issued?: number },
|
|
34
|
+
) => void;
|
|
35
|
+
/** Called before a page request so a workspace can order concurrent follow reads. */
|
|
36
|
+
onreadstart?: () => void;
|
|
37
|
+
/** A row's status text; the listed inbox state is shown when omitted or undefined. */
|
|
38
|
+
status?: (entry: InboxEntry) => string | undefined;
|
|
39
|
+
/** Show the per-row "Handled, no action" control (a workspace resolves in its item pane). */
|
|
40
|
+
resolveInRow?: boolean;
|
|
41
|
+
/** Numbers each page read when its request starts (`issued` on the page). */
|
|
42
|
+
stamp?: () => number;
|
|
22
43
|
}
|
|
23
|
-
let {
|
|
44
|
+
let {
|
|
45
|
+
host,
|
|
46
|
+
onselect,
|
|
47
|
+
rowAside,
|
|
48
|
+
selectedId,
|
|
49
|
+
onitems,
|
|
50
|
+
onreadstart,
|
|
51
|
+
status,
|
|
52
|
+
resolveInRow = true,
|
|
53
|
+
stamp,
|
|
54
|
+
}: Props = $props();
|
|
24
55
|
const { t } = useI18n();
|
|
25
56
|
const states: InboxState[] = [
|
|
26
57
|
'processing',
|
|
@@ -30,11 +61,13 @@ const states: InboxState[] = [
|
|
|
30
61
|
'deferred',
|
|
31
62
|
'partially_completed',
|
|
32
63
|
'completed',
|
|
64
|
+
'resolved',
|
|
33
65
|
];
|
|
34
66
|
let items = $state<InboxEntry[]>([]),
|
|
35
67
|
selectedState = $state(''),
|
|
36
68
|
assignee = $state(''),
|
|
37
69
|
assignments = $state<Record<string, string>>({}),
|
|
70
|
+
reasons = $state<Record<string, string>>({}),
|
|
38
71
|
files = $state<File[]>([]),
|
|
39
72
|
cursor = $state<string>(),
|
|
40
73
|
pending = $state(false),
|
|
@@ -42,41 +75,145 @@ let items = $state<InboxEntry[]>([]),
|
|
|
42
75
|
let alive = true,
|
|
43
76
|
epoch = 0;
|
|
44
77
|
let uploadRequestId: string | undefined;
|
|
78
|
+
// The filter the displayed rows were listed under (the inputs may have been
|
|
79
|
+
// edited since); `patch` keeps a row only while it still matches it.
|
|
80
|
+
let listed: { state?: InboxState; assigneeId?: string } = {};
|
|
81
|
+
function publish(page?: {
|
|
82
|
+
entries: InboxEntry[];
|
|
83
|
+
appended: boolean;
|
|
84
|
+
issued?: number;
|
|
85
|
+
}) {
|
|
86
|
+
onitems?.($state.snapshot(items) as InboxEntry[], !!cursor, page);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Replace one displayed row with a fresher entry for the same item (item and
|
|
90
|
+
* state; the assignment only when its version is newer), keeping its listed
|
|
91
|
+
* label and preview: `load` entries
|
|
92
|
+
* carry no preview and a host may label them differently. Unless `keep` is
|
|
93
|
+
* set, a row that no longer matches the listed filter leaves the list. Rows
|
|
94
|
+
* that are not displayed are never added. Returns the displayed rows.
|
|
95
|
+
*/
|
|
96
|
+
export function patch(entry: InboxEntry, keep = false): InboxEntry[] {
|
|
97
|
+
const index = items.findIndex((row) => row.item.id === entry.item.id);
|
|
98
|
+
if (index < 0) return $state.snapshot(items) as InboxEntry[];
|
|
99
|
+
const previous = items[index];
|
|
100
|
+
// Assignment is versioned (the host's compare-and-set): an entry carrying
|
|
101
|
+
// an older or equal version never replaces the row's, so a view loaded
|
|
102
|
+
// before an assignment cannot roll the row (and the next expected version)
|
|
103
|
+
// back.
|
|
104
|
+
const newer = entry.assignment.version > previous.assignment.version;
|
|
105
|
+
const assignment = newer ? entry.assignment : previous.assignment;
|
|
106
|
+
const matches =
|
|
107
|
+
(!listed.state || entry.state === listed.state) &&
|
|
108
|
+
(!listed.assigneeId || assignment.assigneeId === listed.assigneeId);
|
|
109
|
+
if (keep || matches) {
|
|
110
|
+
const evidence = previous.evidence;
|
|
111
|
+
items[index] = {
|
|
112
|
+
item: entry.item,
|
|
113
|
+
label: previous.label,
|
|
114
|
+
state: entry.state,
|
|
115
|
+
assignment: $state.snapshot(assignment),
|
|
116
|
+
...(evidence ? { evidence } : {}),
|
|
117
|
+
};
|
|
118
|
+
// An unsaved assignee the reviewer typed survives unless the durable
|
|
119
|
+
// assignment itself changed.
|
|
120
|
+
if (newer) assignments[entry.item.id] = assignment.assigneeId ?? '';
|
|
121
|
+
} else items = items.filter((row) => row.item.id !== entry.item.id);
|
|
122
|
+
return $state.snapshot(items) as InboxEntry[];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Append the next page, if any. Resolves with the displayed rows and what
|
|
126
|
+
* happened: `appended` (or `none` when there is no further page), `pending`
|
|
127
|
+
* when another read was in flight or superseded this one (nothing was
|
|
128
|
+
* appended), or `failed` when the page read failed (the list failed closed
|
|
129
|
+
* and its rows were cleared).
|
|
130
|
+
*/
|
|
131
|
+
export async function more(): Promise<{
|
|
132
|
+
items: InboxEntry[];
|
|
133
|
+
result: 'appended' | 'none' | 'pending' | 'failed';
|
|
134
|
+
}> {
|
|
135
|
+
let result: 'appended' | 'none' | 'pending' | 'failed';
|
|
136
|
+
if (pending) result = 'pending';
|
|
137
|
+
else if (!cursor) result = 'none';
|
|
138
|
+
else {
|
|
139
|
+
const read = await load(true);
|
|
140
|
+
result =
|
|
141
|
+
read === 'shown' ? 'appended' : read === 'failed' ? 'failed' : 'pending';
|
|
142
|
+
}
|
|
143
|
+
return { items: $state.snapshot(items) as InboxEntry[], result };
|
|
144
|
+
}
|
|
45
145
|
onDestroy(() => {
|
|
46
146
|
alive = false;
|
|
47
147
|
epoch++;
|
|
48
148
|
files = [];
|
|
49
149
|
uploadRequestId = undefined;
|
|
50
150
|
});
|
|
51
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Resolves `shown` when this read's page was displayed, `failed` when it
|
|
153
|
+
* failed (the rows were cleared), `superseded` when a newer read or the
|
|
154
|
+
* component's end fenced it.
|
|
155
|
+
*/
|
|
156
|
+
async function load(more = false): Promise<'shown' | 'failed' | 'superseded'> {
|
|
52
157
|
const ticket = ++epoch;
|
|
158
|
+
onreadstart?.();
|
|
53
159
|
pending = true;
|
|
54
160
|
error = false;
|
|
55
|
-
|
|
161
|
+
let page:
|
|
162
|
+
| { entries: InboxEntry[]; appended: boolean; issued?: number }
|
|
163
|
+
| undefined;
|
|
56
164
|
const next = more ? cursor : undefined;
|
|
57
|
-
|
|
58
|
-
|
|
165
|
+
// The displayed rows stay (controls disabled while pending) until the new
|
|
166
|
+
// page arrives, so a workspace can keep patching and navigating them; a
|
|
167
|
+
// failed read still clears them.
|
|
168
|
+
const filter = more
|
|
169
|
+
? listed
|
|
170
|
+
: {
|
|
171
|
+
state: selectedState ? (selectedState as InboxState) : undefined,
|
|
172
|
+
assigneeId: assignee || undefined,
|
|
173
|
+
};
|
|
59
174
|
try {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
175
|
+
const issued = stamp?.();
|
|
176
|
+
const read = await host.list({ ...filter, cursor: next });
|
|
177
|
+
if (!alive || ticket !== epoch) return 'superseded';
|
|
178
|
+
listed = filter;
|
|
179
|
+
// An appended page builds on the rows as displayed now (patches made
|
|
180
|
+
// while it loaded included); a row already shown is not repeated.
|
|
181
|
+
const shown = new Set(more ? items.map((entry) => entry.item.id) : []);
|
|
182
|
+
const added = read.items.filter((entry) => !shown.has(entry.item.id));
|
|
183
|
+
page = {
|
|
184
|
+
entries: $state.snapshot(more ? added : read.items) as InboxEntry[],
|
|
185
|
+
appended: more,
|
|
186
|
+
...(issued !== undefined ? { issued } : {}),
|
|
187
|
+
};
|
|
188
|
+
const kept = more ? { assignments, reasons } : undefined;
|
|
189
|
+
items = more ? [...items, ...added] : read.items;
|
|
190
|
+
cursor = read.nextCursor;
|
|
68
191
|
assignments = Object.fromEntries(
|
|
69
|
-
items.map((entry) => [
|
|
192
|
+
items.map((entry) => [
|
|
193
|
+
entry.item.id,
|
|
194
|
+
kept?.assignments[entry.item.id] ?? entry.assignment.assigneeId ?? '',
|
|
195
|
+
]),
|
|
196
|
+
);
|
|
197
|
+
reasons = Object.fromEntries(
|
|
198
|
+
items.map((entry) => [entry.item.id, kept?.reasons[entry.item.id] ?? '']),
|
|
70
199
|
);
|
|
71
200
|
} catch {
|
|
201
|
+
// Any failed read, Load more included, fails closed: the host cannot say
|
|
202
|
+
// whether it was transient or a revoked grant.
|
|
72
203
|
if (alive && ticket === epoch) {
|
|
73
204
|
items = [];
|
|
205
|
+
assignments = {};
|
|
206
|
+
reasons = {};
|
|
74
207
|
cursor = undefined;
|
|
75
208
|
error = true;
|
|
76
209
|
}
|
|
77
210
|
} finally {
|
|
78
|
-
if (alive && ticket === epoch)
|
|
211
|
+
if (alive && ticket === epoch) {
|
|
212
|
+
pending = false;
|
|
213
|
+
publish(page);
|
|
214
|
+
}
|
|
79
215
|
}
|
|
216
|
+
return page ? 'shown' : alive && ticket === epoch ? 'failed' : 'superseded';
|
|
80
217
|
}
|
|
81
218
|
async function mutate(operation: () => Promise<unknown>, selected = false) {
|
|
82
219
|
if (pending) return;
|
|
@@ -99,6 +236,7 @@ async function mutate(operation: () => Promise<unknown>, selected = false) {
|
|
|
99
236
|
assignments = {};
|
|
100
237
|
error = true;
|
|
101
238
|
pending = false;
|
|
239
|
+
publish();
|
|
102
240
|
}
|
|
103
241
|
}
|
|
104
242
|
}
|
|
@@ -126,8 +264,8 @@ onMount(() => {
|
|
|
126
264
|
{#if error}<p role="alert">{t(M['ingestion.error'])}</p>{/if}
|
|
127
265
|
{#if pending}<p role="status">{t(M['ingestion.loading'])}</p>{/if}
|
|
128
266
|
{#if !pending && !error && !items.length}<p>{t(M['ingestion.empty'])}</p>{/if}
|
|
129
|
-
<ul>{#each items as entry (entry.item.id)}<li>
|
|
130
|
-
<h2>{entry.label}</h2><p>{t(M[`ingestion.${entry.state}`])}</p>
|
|
267
|
+
<ul>{#each items as entry (entry.item.id)}<li data-item-id={entry.item.id} aria-current={entry.item.id === selectedId ? 'true' : undefined} class:selected={entry.item.id === selectedId}>
|
|
268
|
+
<h2>{entry.label}</h2><p data-testid="row-status">{status?.(entry) ?? t(M[`ingestion.${entry.state}`])}</p>
|
|
131
269
|
{#if rowAside}<div class="row-aside">{@render rowAside(entry)}</div>{:else if entry.evidence}{@const original = evidenceUrl(entry.evidence.viewUrl)}{@const thumbnail = evidenceUrl(entry.evidence.thumbnailUrl)}{#if thumbnail || original}<div class="row-aside">
|
|
132
270
|
{#if thumbnail}<img src={thumbnail} alt={entry.evidence.label} loading="lazy" />{/if}
|
|
133
271
|
{#if original}<a href={original} target="_blank" rel="noopener noreferrer" aria-label={`${t(M['ingestion.original'])}: ${entry.label}`}>{t(M['ingestion.original'])}</a>{/if}
|
|
@@ -135,12 +273,16 @@ onMount(() => {
|
|
|
135
273
|
<Button onclick={() => onselect?.(entry.item.id)} disabled={pending}>{t(M['ingestion.open'])}</Button>
|
|
136
274
|
<Input aria-label={`${t(M['ingestion.assignee'])}: ${entry.label}`} bind:value={assignments[entry.item.id]} disabled={pending} />
|
|
137
275
|
<Button disabled={pending} onclick={() => mutate(() => host.assign({itemId:entry.item.id,expectedVersion:entry.assignment.version,assigneeId:assignments[entry.item.id] || null,requestId:crypto.randomUUID()}))}>{t(M['ingestion.assign'])}</Button>
|
|
276
|
+
{#if resolveInRow && host.resolve && !entry.item.disposition}
|
|
277
|
+
<Input aria-label={`${t(M['ingestion.resolveReason'])}: ${entry.label}`} bind:value={reasons[entry.item.id]} disabled={pending} />
|
|
278
|
+
<Button disabled={pending} onclick={() => { const close = host.resolve; const reason = reasons[entry.item.id]; if (close) void mutate(() => close({itemId:entry.item.id,disposition:'resolve_no_action',requestId:crypto.randomUUID(),...(reason ? { reason } : {})})); }}>{t(M['ingestion.resolveNoAction'])}</Button>
|
|
279
|
+
{/if}
|
|
138
280
|
</li>{/each}</ul>
|
|
139
281
|
{#if cursor}<Button disabled={pending} onclick={() => load(true)}>{t(M['ingestion.more'])}</Button>{/if}
|
|
140
282
|
</section>
|
|
141
283
|
<style>
|
|
142
284
|
@layer smrt.reset, smrt.structure, smrt.theme;
|
|
143
285
|
@layer smrt.theme {
|
|
144
|
-
section{max-width:72rem;margin-inline:auto;padding:1rem;color:var(--smrt-color-on-surface)}.toolbar{display:flex;flex-wrap:wrap;gap:.75rem;margin-block:1rem}ul{list-style:none;padding:0}li{padding:1rem;border-block-end:var(--smrt-hairline);display:grid;gap:.75rem}h2{font-size:1.1rem}.row-aside{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem}.row-aside img{max-block-size:6rem;max-inline-size:100%;object-fit:contain}
|
|
286
|
+
section{max-width:72rem;margin-inline:auto;padding:1rem;color:var(--smrt-color-on-surface)}.toolbar{display:flex;flex-wrap:wrap;gap:.75rem;margin-block:1rem}ul{list-style:none;padding:0}li{padding:1rem;border-block-end:var(--smrt-hairline);display:grid;gap:.75rem}li.selected{border-inline-start:.25rem solid var(--smrt-color-primary)}h2{font-size:1.1rem}.row-aside{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem}.row-aside img{max-block-size:6rem;max-inline-size:100%;object-fit:contain}
|
|
145
287
|
}
|
|
146
288
|
</style>
|
|
@@ -9,8 +9,35 @@ export interface Props {
|
|
|
9
9
|
* page of rows does not add one complementary landmark per row.
|
|
10
10
|
*/
|
|
11
11
|
rowAside?: Snippet<[InboxEntry]>;
|
|
12
|
+
/** The row a workspace has open; marked `aria-current`. */
|
|
13
|
+
selectedId?: string;
|
|
14
|
+
/**
|
|
15
|
+
* The displayed rows after every settled load or page append (not after
|
|
16
|
+
* `patch`, which returns them), whether more pages exist, and the entries
|
|
17
|
+
* exactly as the page read returned them (`appended` for Load more), absent
|
|
18
|
+
* when the read failed.
|
|
19
|
+
*/
|
|
20
|
+
onitems?: (items: InboxEntry[], more: boolean, page?: {
|
|
21
|
+
entries: InboxEntry[];
|
|
22
|
+
appended: boolean;
|
|
23
|
+
issued?: number;
|
|
24
|
+
}) => void;
|
|
25
|
+
/** Called before a page request so a workspace can order concurrent follow reads. */
|
|
26
|
+
onreadstart?: () => void;
|
|
27
|
+
/** A row's status text; the listed inbox state is shown when omitted or undefined. */
|
|
28
|
+
status?: (entry: InboxEntry) => string | undefined;
|
|
29
|
+
/** Show the per-row "Handled, no action" control (a workspace resolves in its item pane). */
|
|
30
|
+
resolveInRow?: boolean;
|
|
31
|
+
/** Numbers each page read when its request starts (`issued` on the page). */
|
|
32
|
+
stamp?: () => number;
|
|
12
33
|
}
|
|
13
|
-
declare const InboxView: import("svelte").Component<Props, {
|
|
34
|
+
declare const InboxView: import("svelte").Component<Props, {
|
|
35
|
+
patch: (entry: InboxEntry, keep?: boolean) => InboxEntry[];
|
|
36
|
+
more: () => Promise<{
|
|
37
|
+
items: InboxEntry[];
|
|
38
|
+
result: "appended" | "none" | "pending" | "failed";
|
|
39
|
+
}>;
|
|
40
|
+
}, "">;
|
|
14
41
|
type InboxView = ReturnType<typeof InboxView>;
|
|
15
42
|
export default InboxView;
|
|
16
43
|
//# sourceMappingURL=InboxView.svelte.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InboxView.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/InboxView.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,EAAsB,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC1D,OAAO,KAAK,EACV,UAAU,EAEV,gBAAgB,EACjB,MAAM,qBAAqB,CAAC;AAI7B,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"InboxView.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/InboxView.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,EAAsB,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC1D,OAAO,KAAK,EACV,UAAU,EAEV,gBAAgB,EACjB,MAAM,qBAAqB,CAAC;AAI7B,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;IACjC,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CACR,KAAK,EAAE,UAAU,EAAE,EACnB,IAAI,EAAE,OAAO,EACb,IAAI,CAAC,EAAE;QAAE,OAAO,EAAE,UAAU,EAAE,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,KACjE,IAAI,CAAC;IACV,qFAAqF;IACrF,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,sFAAsF;IACtF,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,MAAM,GAAG,SAAS,CAAC;IACnD,6FAA6F;IAC7F,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;CACtB;AA6PD,QAAA,MAAM,SAAS;mBA/LQ,UAAU,qBAAiB,UAAU,EAAE;gBAmCtC,OAAO,CAAC;QAC9B,KAAK,EAAE,UAAU,EAAE,CAAC;QACpB,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;KACpD,CAAC;MAyJqD,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { InboxEntry, IntakeReviewHost } from '../../review-dto.js';
|
|
4
|
+
import TriageView from './TriageView.svelte';
|
|
5
|
+
export interface Props {
|
|
6
|
+
host: IntakeReviewHost;
|
|
7
|
+
/** Replace whenever the authenticated tenant/session context changes. */
|
|
8
|
+
contextKey?: string;
|
|
9
|
+
/** Item to open first; afterwards the workspace owns the selection. */
|
|
10
|
+
itemId?: string;
|
|
11
|
+
/** Reload interval while an item's background stage is in flight. */
|
|
12
|
+
pollMs?: number;
|
|
13
|
+
/** Told about every selection change (e.g. to keep a URL in step); never navigates. */
|
|
14
|
+
onselect?: (itemId: string | undefined) => void;
|
|
15
|
+
/** Optional per-row aside (preview, original link); receives the page entry. */
|
|
16
|
+
rowAside?: Snippet<[InboxEntry]>;
|
|
17
|
+
}
|
|
18
|
+
let {
|
|
19
|
+
host,
|
|
20
|
+
contextKey = '',
|
|
21
|
+
itemId,
|
|
22
|
+
pollMs = 2000,
|
|
23
|
+
onselect,
|
|
24
|
+
rowAside,
|
|
25
|
+
}: Props = $props();
|
|
26
|
+
</script>
|
|
27
|
+
{#key host}{#key contextKey}<TriageView {host} {itemId} {pollMs} {onselect} {rowAside} />{/key}{/key}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { InboxEntry, IntakeReviewHost } from '../../review-dto.js';
|
|
3
|
+
export interface Props {
|
|
4
|
+
host: IntakeReviewHost;
|
|
5
|
+
/** Replace whenever the authenticated tenant/session context changes. */
|
|
6
|
+
contextKey?: string;
|
|
7
|
+
/** Item to open first; afterwards the workspace owns the selection. */
|
|
8
|
+
itemId?: string;
|
|
9
|
+
/** Reload interval while an item's background stage is in flight. */
|
|
10
|
+
pollMs?: number;
|
|
11
|
+
/** Told about every selection change (e.g. to keep a URL in step); never navigates. */
|
|
12
|
+
onselect?: (itemId: string | undefined) => void;
|
|
13
|
+
/** Optional per-row aside (preview, original link); receives the page entry. */
|
|
14
|
+
rowAside?: Snippet<[InboxEntry]>;
|
|
15
|
+
}
|
|
16
|
+
declare const IntakeWorkspace: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type IntakeWorkspace = ReturnType<typeof IntakeWorkspace>;
|
|
18
|
+
export default IntakeWorkspace;
|
|
19
|
+
//# sourceMappingURL=IntakeWorkspace.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IntakeWorkspace.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/IntakeWorkspace.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAGxE,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;IAChD,gFAAgF;IAChF,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;CAClC;AAmBD,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|