@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.
Files changed (83) hide show
  1. package/FEEDBACK.md +104 -0
  2. package/PROPOSALS.md +19 -0
  3. package/README.md +126 -0
  4. package/REVIEW.md +272 -4
  5. package/agents/execution.md +72 -0
  6. package/dist/chunks/{execution-9CfjxX8I.js → execution-BzVQax0n.js} +421 -23
  7. package/dist/chunks/execution-BzVQax0n.js.map +1 -0
  8. package/dist/chunks/{feedback-CuUzWKzP.js → feedback-D3UTF8fm.js} +119 -21
  9. package/dist/chunks/feedback-D3UTF8fm.js.map +1 -0
  10. package/dist/chunks/{extraction-CJ-zrbg0.js → proposal-contracts-77Uo1B1j.js} +39 -39
  11. package/dist/chunks/proposal-contracts-77Uo1B1j.js.map +1 -0
  12. package/dist/chunks/{reviewer-instruction-Db3jDN9a.js → reviewer-instruction-CEc3AOuU.js} +2 -2
  13. package/dist/chunks/{reviewer-instruction-Db3jDN9a.js.map → reviewer-instruction-CEc3AOuU.js.map} +1 -1
  14. package/dist/dto.d.ts +69 -0
  15. package/dist/dto.d.ts.map +1 -1
  16. package/dist/execution-contracts.d.ts +4 -1
  17. package/dist/execution-contracts.d.ts.map +1 -1
  18. package/dist/execution-dto.d.ts +16 -0
  19. package/dist/execution-dto.d.ts.map +1 -1
  20. package/dist/execution.d.ts +58 -3
  21. package/dist/execution.d.ts.map +1 -1
  22. package/dist/execution.js +2 -2
  23. package/dist/feedback-dto.d.ts +12 -4
  24. package/dist/feedback-dto.d.ts.map +1 -1
  25. package/dist/feedback-internal.d.ts +9 -0
  26. package/dist/feedback-internal.d.ts.map +1 -1
  27. package/dist/feedback.d.ts +3 -0
  28. package/dist/feedback.d.ts.map +1 -1
  29. package/dist/index.js +2 -2
  30. package/dist/manifest.json +321 -3
  31. package/dist/models.d.ts +15 -0
  32. package/dist/models.d.ts.map +1 -1
  33. package/dist/models.js +73 -2
  34. package/dist/models.js.map +1 -1
  35. package/dist/policy.d.ts +3 -0
  36. package/dist/policy.d.ts.map +1 -1
  37. package/dist/proposal-errors.d.ts +4 -0
  38. package/dist/proposal-errors.d.ts.map +1 -1
  39. package/dist/proposals.d.ts +1 -0
  40. package/dist/proposals.d.ts.map +1 -1
  41. package/dist/proposals.js +11 -6
  42. package/dist/proposals.js.map +1 -1
  43. package/dist/redaction.d.ts +9 -0
  44. package/dist/redaction.d.ts.map +1 -0
  45. package/dist/review-dto.d.ts +14 -4
  46. package/dist/review-dto.d.ts.map +1 -1
  47. package/dist/review-host.d.ts.map +1 -1
  48. package/dist/server.d.ts +82 -10
  49. package/dist/server.d.ts.map +1 -1
  50. package/dist/server.js +366 -47
  51. package/dist/server.js.map +1 -1
  52. package/dist/smrt-knowledge.json +124 -18
  53. package/dist/svelte/components/ActionReview.svelte +39 -9
  54. package/dist/svelte/components/ActionReview.svelte.d.ts +8 -1
  55. package/dist/svelte/components/ActionReview.svelte.d.ts.map +1 -1
  56. package/dist/svelte/components/EvidenceViewer.svelte +60 -4
  57. package/dist/svelte/components/EvidenceViewer.svelte.d.ts +12 -1
  58. package/dist/svelte/components/EvidenceViewer.svelte.d.ts.map +1 -1
  59. package/dist/svelte/components/InboxView.svelte +160 -18
  60. package/dist/svelte/components/InboxView.svelte.d.ts +28 -1
  61. package/dist/svelte/components/InboxView.svelte.d.ts.map +1 -1
  62. package/dist/svelte/components/IntakeWorkspace.svelte +27 -0
  63. package/dist/svelte/components/IntakeWorkspace.svelte.d.ts +19 -0
  64. package/dist/svelte/components/IntakeWorkspace.svelte.d.ts.map +1 -0
  65. package/dist/svelte/components/ReviewView.svelte +264 -23
  66. package/dist/svelte/components/ReviewView.svelte.d.ts +18 -1
  67. package/dist/svelte/components/ReviewView.svelte.d.ts.map +1 -1
  68. package/dist/svelte/components/TriageView.svelte +669 -0
  69. package/dist/svelte/components/TriageView.svelte.d.ts +16 -0
  70. package/dist/svelte/components/TriageView.svelte.d.ts.map +1 -0
  71. package/dist/svelte/i18n.d.ts +39 -0
  72. package/dist/svelte/i18n.d.ts.map +1 -1
  73. package/dist/svelte/i18n.js +39 -0
  74. package/dist/svelte/index.d.ts +1 -0
  75. package/dist/svelte/index.d.ts.map +1 -1
  76. package/dist/svelte/index.js +1 -0
  77. package/dist/svelte/view-utils.d.ts +65 -0
  78. package/dist/svelte/view-utils.d.ts.map +1 -1
  79. package/dist/svelte/view-utils.js +145 -0
  80. package/package.json +13 -13
  81. package/dist/chunks/execution-9CfjxX8I.js.map +0 -1
  82. package/dist/chunks/extraction-CJ-zrbg0.js.map +0 -1
  83. 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
- run: (operation: () => Promise<unknown>) => Promise<void>;
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 { host, itemId, action, catalog, currentAttemptId, run, deny }: Props =
22
- $props();
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><Button onclick={() => decide('defer')}>{t(M['ingestion.defer'])}</Button>{/if}</div>
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={dirty} onclick={() => { if (!dirty) void run(() => host.apply(action.review.actionId)); }}>{t(M['ingestion.apply'])}</Button>{/if}
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
- run: (operation: () => Promise<unknown>) => Promise<void>;
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":"AAQA,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,GAAG,EAAE,CAAC,SAAS,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAmMD,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
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 { evidence }: Props = $props();
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
- {#each evidence as view (view.evidence.id)}
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'}<iframe src={url} title={view.label}></iframe>
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;CAC1B;AA+BD,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,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 { host, onselect, rowAside }: Props = $props();
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
- async function load(more = false) {
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
- const prior = more ? items : [];
161
+ let page:
162
+ | { entries: InboxEntry[]; appended: boolean; issued?: number }
163
+ | undefined;
56
164
  const next = more ? cursor : undefined;
57
- items = [];
58
- assignments = {};
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 page = await host.list({
61
- state: selectedState ? (selectedState as InboxState) : undefined,
62
- assigneeId: assignee || undefined,
63
- cursor: next,
64
- });
65
- if (!alive || ticket !== epoch) return;
66
- items = [...prior, ...page.items];
67
- cursor = page.nextCursor;
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) => [entry.item.id, entry.assignment.assigneeId ?? '']),
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) pending = false;
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;CAClC;AAoID,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,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"}