@happyvertical/smrt-ingestion 0.55.6 → 0.55.8
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/AGENTS.md +13 -3
- package/FEEDBACK.md +156 -0
- package/README.md +15 -3
- package/REVIEW.md +137 -0
- package/dist/chunks/{execution-C6HJvl9e.js → execution-C7sTFXFU.js} +375 -11
- package/dist/chunks/execution-C7sTFXFU.js.map +1 -0
- package/dist/chunks/execution-internal-DWteDHM2.js.map +1 -1
- package/dist/chunks/feedback-C3OHD4sA.js +625 -0
- package/dist/chunks/feedback-C3OHD4sA.js.map +1 -0
- package/dist/chunks/{extraction-bseWZuWN.js → proposal-errors-z24VKWnQ.js} +10 -2
- package/dist/chunks/proposal-errors-z24VKWnQ.js.map +1 -0
- package/dist/dto.d.ts +2 -0
- package/dist/dto.d.ts.map +1 -1
- package/dist/execution-internal.d.ts +3 -1
- package/dist/execution-internal.d.ts.map +1 -1
- package/dist/execution.d.ts +15 -1
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js +1 -1
- package/dist/feedback-contracts.d.ts +51 -0
- package/dist/feedback-contracts.d.ts.map +1 -0
- package/dist/feedback-dto.d.ts +106 -0
- package/dist/feedback-dto.d.ts.map +1 -0
- package/dist/feedback-internal.d.ts +19 -0
- package/dist/feedback-internal.d.ts.map +1 -0
- package/dist/feedback.d.ts +54 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/manifest.json +34 -1
- package/dist/models.js +1 -1
- package/dist/models.js.map +1 -1
- package/dist/proposal-contracts.d.ts +4 -0
- package/dist/proposal-contracts.d.ts.map +1 -1
- package/dist/proposal-dto.d.ts +4 -0
- package/dist/proposal-dto.d.ts.map +1 -1
- package/dist/proposal-errors.d.ts +5 -0
- package/dist/proposal-errors.d.ts.map +1 -0
- package/dist/proposal-sdk.d.ts.map +1 -1
- package/dist/proposals.d.ts +4 -1
- package/dist/proposals.d.ts.map +1 -1
- package/dist/proposals.js +114 -15
- package/dist/proposals.js.map +1 -1
- package/dist/review-dto.d.ts +115 -0
- package/dist/review-dto.d.ts.map +1 -0
- package/dist/server.d.ts +24 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +146 -54
- package/dist/server.js.map +1 -1
- package/dist/smrt-knowledge.json +97 -14
- package/dist/svelte/components/ActionReview.svelte +232 -0
- package/dist/svelte/components/ActionReview.svelte.d.ts +15 -0
- package/dist/svelte/components/ActionReview.svelte.d.ts.map +1 -0
- package/dist/svelte/components/EvidenceViewer.svelte +31 -0
- package/dist/svelte/components/EvidenceViewer.svelte.d.ts +8 -0
- package/dist/svelte/components/EvidenceViewer.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InboxView.svelte +119 -0
- package/dist/svelte/components/InboxView.svelte.d.ts +9 -0
- package/dist/svelte/components/InboxView.svelte.d.ts.map +1 -0
- package/dist/svelte/components/IntakeInbox.svelte +11 -0
- package/dist/svelte/components/IntakeInbox.svelte.d.ts +10 -0
- package/dist/svelte/components/IntakeInbox.svelte.d.ts.map +1 -0
- package/dist/svelte/components/IntakeReview.svelte +11 -0
- package/dist/svelte/components/IntakeReview.svelte.d.ts +10 -0
- package/dist/svelte/components/IntakeReview.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ReviewView.svelte +243 -0
- package/dist/svelte/components/ReviewView.svelte.d.ts +9 -0
- package/dist/svelte/components/ReviewView.svelte.d.ts.map +1 -0
- package/dist/svelte/i18n.d.ts +72 -0
- package/dist/svelte/i18n.d.ts.map +1 -0
- package/dist/svelte/i18n.js +72 -0
- package/dist/svelte/index.d.ts +5 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +3 -0
- package/dist/svelte/view-utils.d.ts +13 -0
- package/dist/svelte/view-utils.d.ts.map +1 -0
- package/dist/svelte/view-utils.js +96 -0
- package/package.json +49 -22
- package/dist/chunks/execution-C6HJvl9e.js.map +0 -1
- package/dist/chunks/extraction-bseWZuWN.js.map +0 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ProposalCatalogEntry } from '../../proposal-dto.js';
|
|
2
|
+
import type { IntakeReviewHost, ReviewAction } from '../../review-dto.js';
|
|
3
|
+
export interface Props {
|
|
4
|
+
host: IntakeReviewHost;
|
|
5
|
+
itemId: string;
|
|
6
|
+
action: ReviewAction;
|
|
7
|
+
catalog?: ProposalCatalogEntry;
|
|
8
|
+
currentAttemptId?: string;
|
|
9
|
+
run: (operation: () => Promise<unknown>) => Promise<void>;
|
|
10
|
+
deny: () => void;
|
|
11
|
+
}
|
|
12
|
+
declare const ActionReview: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type ActionReview = ReturnType<typeof ActionReview>;
|
|
14
|
+
export default ActionReview;
|
|
15
|
+
//# sourceMappingURL=ActionReview.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ActionReview.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ActionReview.svelte.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAEV,oBAAoB,EACrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAI1E,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;AA+ND,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
3
|
+
import { Button } from '@happyvertical/smrt-ui/ui';
|
|
4
|
+
import type { EvidenceView } from '../../review-dto.js';
|
|
5
|
+
import { M } from '../i18n.js';
|
|
6
|
+
import { evidenceUrl } from '../view-utils.js';
|
|
7
|
+
export interface Props {
|
|
8
|
+
evidence: EvidenceView[];
|
|
9
|
+
}
|
|
10
|
+
let { evidence }: Props = $props();
|
|
11
|
+
const { t } = useI18n();
|
|
12
|
+
</script>
|
|
13
|
+
<section data-testid="evidence-viewer" aria-label={t(M['ingestion.evidence'])}>
|
|
14
|
+
<h2>{t(M['ingestion.evidence'])}</h2>
|
|
15
|
+
{#each evidence as view (view.evidence.id)}
|
|
16
|
+
{@const url = evidenceUrl(view.viewUrl)}
|
|
17
|
+
<article id={`evidence-${view.evidence.id}`}>
|
|
18
|
+
<h3>{view.label}</h3>
|
|
19
|
+
{#if url}
|
|
20
|
+
{#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>
|
|
22
|
+
{:else if view.evidence.mediaType.startsWith('audio/')}<audio controls src={url} aria-label={view.label}></audio>{/if}
|
|
23
|
+
<Button href={url} target="_blank" rel="noopener noreferrer">{t(M['ingestion.original'])}</Button>
|
|
24
|
+
{:else}<p>{t(M['ingestion.noOriginal'])}</p>{/if}
|
|
25
|
+
{#if view.text !== undefined}<pre>{view.text}</pre>{/if}
|
|
26
|
+
</article>
|
|
27
|
+
{/each}
|
|
28
|
+
</section>
|
|
29
|
+
<style>
|
|
30
|
+
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:1px solid var(--smrt-color-outline-variant,#ccc)}pre{white-space:pre-wrap;overflow-wrap:anywhere}
|
|
31
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EvidenceView } from '../../review-dto.js';
|
|
2
|
+
export interface Props {
|
|
3
|
+
evidence: EvidenceView[];
|
|
4
|
+
}
|
|
5
|
+
declare const EvidenceViewer: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type EvidenceViewer = ReturnType<typeof EvidenceViewer>;
|
|
7
|
+
export default EvidenceViewer;
|
|
8
|
+
//# sourceMappingURL=EvidenceViewer.svelte.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { FilePicker, Input, Select } from '@happyvertical/smrt-ui/forms';
|
|
3
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
4
|
+
import { Button } from '@happyvertical/smrt-ui/ui';
|
|
5
|
+
import { onDestroy, onMount } from 'svelte';
|
|
6
|
+
import type {
|
|
7
|
+
InboxEntry,
|
|
8
|
+
InboxState,
|
|
9
|
+
IntakeReviewHost,
|
|
10
|
+
} from '../../review-dto.js';
|
|
11
|
+
import { M } from '../i18n.js';
|
|
12
|
+
export interface Props {
|
|
13
|
+
host: IntakeReviewHost;
|
|
14
|
+
onselect?: (itemId: string) => void;
|
|
15
|
+
}
|
|
16
|
+
let { host, onselect }: Props = $props();
|
|
17
|
+
const { t } = useI18n();
|
|
18
|
+
const states: InboxState[] = [
|
|
19
|
+
'processing',
|
|
20
|
+
'unresolved',
|
|
21
|
+
'waiting',
|
|
22
|
+
'failed',
|
|
23
|
+
'deferred',
|
|
24
|
+
'partially_completed',
|
|
25
|
+
'completed',
|
|
26
|
+
];
|
|
27
|
+
let items = $state<InboxEntry[]>([]),
|
|
28
|
+
selectedState = $state(''),
|
|
29
|
+
assignee = $state(''),
|
|
30
|
+
assignments = $state<Record<string, string>>({}),
|
|
31
|
+
files = $state<File[]>([]),
|
|
32
|
+
cursor = $state<string>(),
|
|
33
|
+
pending = $state(false),
|
|
34
|
+
error = $state(false);
|
|
35
|
+
let alive = true,
|
|
36
|
+
epoch = 0;
|
|
37
|
+
onDestroy(() => {
|
|
38
|
+
alive = false;
|
|
39
|
+
epoch++;
|
|
40
|
+
});
|
|
41
|
+
async function load(more = false) {
|
|
42
|
+
const ticket = ++epoch;
|
|
43
|
+
pending = true;
|
|
44
|
+
error = false;
|
|
45
|
+
const prior = more ? items : [];
|
|
46
|
+
const next = more ? cursor : undefined;
|
|
47
|
+
items = [];
|
|
48
|
+
assignments = {};
|
|
49
|
+
try {
|
|
50
|
+
const page = await host.list({
|
|
51
|
+
state: selectedState ? (selectedState as InboxState) : undefined,
|
|
52
|
+
assigneeId: assignee || undefined,
|
|
53
|
+
cursor: next,
|
|
54
|
+
});
|
|
55
|
+
if (!alive || ticket !== epoch) return;
|
|
56
|
+
items = [...prior, ...page.items];
|
|
57
|
+
cursor = page.nextCursor;
|
|
58
|
+
assignments = Object.fromEntries(
|
|
59
|
+
items.map((entry) => [entry.item.id, entry.assignment.assigneeId ?? '']),
|
|
60
|
+
);
|
|
61
|
+
} catch {
|
|
62
|
+
if (alive && ticket === epoch) {
|
|
63
|
+
items = [];
|
|
64
|
+
cursor = undefined;
|
|
65
|
+
error = true;
|
|
66
|
+
}
|
|
67
|
+
} finally {
|
|
68
|
+
if (alive && ticket === epoch) pending = false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function mutate(operation: () => Promise<unknown>, selected = false) {
|
|
72
|
+
if (pending) return;
|
|
73
|
+
pending = true;
|
|
74
|
+
error = false;
|
|
75
|
+
const ticket = ++epoch;
|
|
76
|
+
try {
|
|
77
|
+
const result = await operation();
|
|
78
|
+
if (!alive || ticket !== epoch) return;
|
|
79
|
+
files = [];
|
|
80
|
+
if (selected && result && typeof result === 'object' && 'itemId' in result)
|
|
81
|
+
onselect?.(String(result.itemId));
|
|
82
|
+
await load();
|
|
83
|
+
} catch {
|
|
84
|
+
if (alive && ticket === epoch) {
|
|
85
|
+
items = [];
|
|
86
|
+
assignments = {};
|
|
87
|
+
files = [];
|
|
88
|
+
error = true;
|
|
89
|
+
pending = false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
onMount(() => {
|
|
94
|
+
void load();
|
|
95
|
+
});
|
|
96
|
+
</script>
|
|
97
|
+
<section data-testid="intake-inbox" aria-busy={pending}>
|
|
98
|
+
<h1>{t(M['ingestion.inbox'])}</h1>
|
|
99
|
+
<div class="toolbar">
|
|
100
|
+
<Select aria-label={t(M['ingestion.filter'])} bind:value={selectedState} disabled={pending} onchange={() => load()}><option value="">{t(M['ingestion.all'])}</option>{#each states as value}<option {value}>{t(M[`ingestion.${value}`])}</option>{/each}</Select>
|
|
101
|
+
<Input aria-label={t(M['ingestion.assignee'])} bind:value={assignee} disabled={pending} />
|
|
102
|
+
<Button onclick={() => load()} disabled={pending}>{t(M['ingestion.refresh'])}</Button>
|
|
103
|
+
</div>
|
|
104
|
+
<FilePicker bind:files multiple label={t(M['ingestion.files'])} description={t(M['ingestion.filesHelp'])} disabled={pending} />
|
|
105
|
+
<Button disabled={pending || !files.length} onclick={() => mutate(() => host.upload([...files],crypto.randomUUID()),true)}>{t(M['ingestion.upload'])}</Button>
|
|
106
|
+
{#if error}<p role="alert">{t(M['ingestion.error'])}</p>{/if}
|
|
107
|
+
{#if pending}<p role="status">{t(M['ingestion.loading'])}</p>{/if}
|
|
108
|
+
{#if !pending && !error && !items.length}<p>{t(M['ingestion.empty'])}</p>{/if}
|
|
109
|
+
<ul>{#each items as entry (entry.item.id)}<li>
|
|
110
|
+
<h2>{entry.label}</h2><p>{t(M[`ingestion.${entry.state}`])}</p>
|
|
111
|
+
<Button onclick={() => onselect?.(entry.item.id)} disabled={pending}>{t(M['ingestion.open'])}</Button>
|
|
112
|
+
<Input aria-label={`${t(M['ingestion.assignee'])}: ${entry.label}`} bind:value={assignments[entry.item.id]} disabled={pending} />
|
|
113
|
+
<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>
|
|
114
|
+
</li>{/each}</ul>
|
|
115
|
+
{#if cursor}<Button disabled={pending} onclick={() => load(true)}>{t(M['ingestion.more'])}</Button>{/if}
|
|
116
|
+
</section>
|
|
117
|
+
<style>
|
|
118
|
+
section{max-width:72rem;margin-inline:auto;padding:1rem;color:var(--smrt-color-on-surface,#222)}.toolbar{display:flex;flex-wrap:wrap;gap:.75rem;margin-block:1rem}ul{list-style:none;padding:0}li{padding:1rem;border-block-end:1px solid var(--smrt-color-outline-variant,#ccc);display:grid;gap:.75rem}h2{font-size:1.1rem}
|
|
119
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { IntakeReviewHost } from '../../review-dto.js';
|
|
2
|
+
export interface Props {
|
|
3
|
+
host: IntakeReviewHost;
|
|
4
|
+
onselect?: (itemId: string) => void;
|
|
5
|
+
}
|
|
6
|
+
declare const InboxView: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type InboxView = ReturnType<typeof InboxView>;
|
|
8
|
+
export default InboxView;
|
|
9
|
+
//# sourceMappingURL=InboxView.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InboxView.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/InboxView.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAGV,gBAAgB,EACjB,MAAM,qBAAqB,CAAC;AAG7B,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAkHD,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { IntakeReviewHost } from '../../review-dto.js';
|
|
3
|
+
import InboxView from './InboxView.svelte';
|
|
4
|
+
export interface Props {
|
|
5
|
+
host: IntakeReviewHost;
|
|
6
|
+
contextKey?: string;
|
|
7
|
+
onselect?: (itemId: string) => void;
|
|
8
|
+
}
|
|
9
|
+
let { host, contextKey = '', onselect }: Props = $props();
|
|
10
|
+
</script>
|
|
11
|
+
{#key host}{#key contextKey}<InboxView {host} {onselect} />{/key}{/key}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IntakeReviewHost } from '../../review-dto.js';
|
|
2
|
+
export interface Props {
|
|
3
|
+
host: IntakeReviewHost;
|
|
4
|
+
contextKey?: string;
|
|
5
|
+
onselect?: (itemId: string) => void;
|
|
6
|
+
}
|
|
7
|
+
declare const IntakeInbox: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type IntakeInbox = ReturnType<typeof IntakeInbox>;
|
|
9
|
+
export default IntakeInbox;
|
|
10
|
+
//# sourceMappingURL=IntakeInbox.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IntakeInbox.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/IntakeInbox.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAG5D,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAWD,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { IntakeReviewHost } from '../../review-dto.js';
|
|
3
|
+
import ReviewView from './ReviewView.svelte';
|
|
4
|
+
export interface Props {
|
|
5
|
+
host: IntakeReviewHost;
|
|
6
|
+
itemId: string;
|
|
7
|
+
contextKey?: string;
|
|
8
|
+
}
|
|
9
|
+
let { host, itemId, contextKey = '' }: Props = $props();
|
|
10
|
+
</script>
|
|
11
|
+
{#key host}{#key contextKey}{#key itemId}<ReviewView {host} {itemId} />{/key}{/key}{/key}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IntakeReviewHost } from '../../review-dto.js';
|
|
2
|
+
export interface Props {
|
|
3
|
+
host: IntakeReviewHost;
|
|
4
|
+
itemId: string;
|
|
5
|
+
contextKey?: string;
|
|
6
|
+
}
|
|
7
|
+
declare const IntakeReview: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type IntakeReview = ReturnType<typeof IntakeReview>;
|
|
9
|
+
export default IntakeReview;
|
|
10
|
+
//# sourceMappingURL=IntakeReview.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IntakeReview.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/IntakeReview.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAG5D,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAWD,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Textarea } from '@happyvertical/smrt-ui/forms';
|
|
3
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
4
|
+
import { Button } from '@happyvertical/smrt-ui/ui';
|
|
5
|
+
import { onDestroy, onMount } from 'svelte';
|
|
6
|
+
import type { EvidenceLocation } from '../../extraction-types.js';
|
|
7
|
+
import type { IntakeReviewHost, ItemReviewView } from '../../review-dto.js';
|
|
8
|
+
import { M } from '../i18n.js';
|
|
9
|
+
import { evidenceUrl, extractedContent, pageGroups } from '../view-utils.js';
|
|
10
|
+
import ActionReview from './ActionReview.svelte';
|
|
11
|
+
import EvidenceViewer from './EvidenceViewer.svelte';
|
|
12
|
+
export interface Props {
|
|
13
|
+
host: IntakeReviewHost;
|
|
14
|
+
itemId: string;
|
|
15
|
+
}
|
|
16
|
+
let { host, itemId }: Props = $props();
|
|
17
|
+
const { t } = useI18n();
|
|
18
|
+
let view = $state<ItemReviewView>(),
|
|
19
|
+
pending = $state(false),
|
|
20
|
+
error = $state(false),
|
|
21
|
+
splits = $state<Record<string, string>>({}),
|
|
22
|
+
invalid = $state(false);
|
|
23
|
+
let alive = true,
|
|
24
|
+
epoch = 0,
|
|
25
|
+
loadedPages = 1;
|
|
26
|
+
onDestroy(() => {
|
|
27
|
+
alive = false;
|
|
28
|
+
epoch++;
|
|
29
|
+
});
|
|
30
|
+
function deny() {
|
|
31
|
+
epoch++;
|
|
32
|
+
view = undefined;
|
|
33
|
+
splits = {};
|
|
34
|
+
error = true;
|
|
35
|
+
pending = false;
|
|
36
|
+
}
|
|
37
|
+
async function load(pageCount = loadedPages) {
|
|
38
|
+
const ticket = ++epoch;
|
|
39
|
+
view = undefined;
|
|
40
|
+
splits = {};
|
|
41
|
+
pending = true;
|
|
42
|
+
error = false;
|
|
43
|
+
invalid = false;
|
|
44
|
+
try {
|
|
45
|
+
let cursor: string | undefined;
|
|
46
|
+
let result: ItemReviewView | undefined;
|
|
47
|
+
const actions: ItemReviewView['reviews']['actions'] = [];
|
|
48
|
+
const actionIds = new Set<string>();
|
|
49
|
+
const cursors = new Set<string>();
|
|
50
|
+
let refreshedPages = 0;
|
|
51
|
+
for (let index = 0; index < pageCount; index++) {
|
|
52
|
+
const page = await host.load(itemId, cursor);
|
|
53
|
+
if (!alive || ticket !== epoch) return;
|
|
54
|
+
if (page.availability !== 'available') {
|
|
55
|
+
result = page;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
if (
|
|
59
|
+
result &&
|
|
60
|
+
page.entry.item.analysisRevision !== result.entry.item.analysisRevision
|
|
61
|
+
)
|
|
62
|
+
throw new Error('Review changed during pagination');
|
|
63
|
+
result ??= page;
|
|
64
|
+
for (const action of page.reviews.actions) {
|
|
65
|
+
if (!actionIds.has(action.review.actionId)) {
|
|
66
|
+
actionIds.add(action.review.actionId);
|
|
67
|
+
actions.push(action);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
refreshedPages++;
|
|
71
|
+
cursor = page.reviews.nextCursor;
|
|
72
|
+
if (!cursor) break;
|
|
73
|
+
if (cursors.has(cursor)) throw new Error('Repeated review cursor');
|
|
74
|
+
cursors.add(cursor);
|
|
75
|
+
}
|
|
76
|
+
if (!result) throw new Error('Missing review page');
|
|
77
|
+
if (result.availability === 'available') {
|
|
78
|
+
result = {
|
|
79
|
+
...result,
|
|
80
|
+
reviews: { ...result.reviews, actions, nextCursor: cursor },
|
|
81
|
+
};
|
|
82
|
+
loadedPages = refreshedPages;
|
|
83
|
+
}
|
|
84
|
+
// A host denial must not leave any original, candidate, or argument in the DOM.
|
|
85
|
+
view =
|
|
86
|
+
result.availability === 'available'
|
|
87
|
+
? result
|
|
88
|
+
: {
|
|
89
|
+
entry: result.entry,
|
|
90
|
+
evidence: [],
|
|
91
|
+
reviews: { actions: [] },
|
|
92
|
+
availability: result.availability,
|
|
93
|
+
};
|
|
94
|
+
splits = Object.fromEntries(
|
|
95
|
+
(view.generation?.splits ?? []).map((split) => [
|
|
96
|
+
split.evidenceId,
|
|
97
|
+
JSON.stringify(split.groups),
|
|
98
|
+
]),
|
|
99
|
+
);
|
|
100
|
+
} catch {
|
|
101
|
+
if (alive && ticket === epoch) deny();
|
|
102
|
+
} finally {
|
|
103
|
+
if (alive && ticket === epoch) pending = false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function run(operation: () => Promise<unknown>) {
|
|
107
|
+
if (pending) return;
|
|
108
|
+
const ticket = ++epoch;
|
|
109
|
+
pending = true;
|
|
110
|
+
error = false;
|
|
111
|
+
view = undefined;
|
|
112
|
+
splits = {};
|
|
113
|
+
try {
|
|
114
|
+
await operation();
|
|
115
|
+
if (alive && ticket === epoch) await load();
|
|
116
|
+
} catch {
|
|
117
|
+
if (alive && ticket === epoch) deny();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function preview(index: number) {
|
|
121
|
+
const analysis = view?.analysis;
|
|
122
|
+
const suggestion = view?.generation?.suggestions[index];
|
|
123
|
+
if (!analysis || suggestion?.disposition !== 'ready_for_review') return;
|
|
124
|
+
const input = {
|
|
125
|
+
itemId,
|
|
126
|
+
attemptId: analysis.attemptId,
|
|
127
|
+
index,
|
|
128
|
+
requestId: crypto.randomUUID(),
|
|
129
|
+
};
|
|
130
|
+
void run(() => host.preview(input));
|
|
131
|
+
}
|
|
132
|
+
function split(evidenceId: string) {
|
|
133
|
+
const analysis = view?.analysis;
|
|
134
|
+
if (!view?.generation || !analysis) return;
|
|
135
|
+
let groups: number[][];
|
|
136
|
+
try {
|
|
137
|
+
groups = pageGroups(splits[evidenceId]);
|
|
138
|
+
} catch {
|
|
139
|
+
invalid = true;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const input = {
|
|
143
|
+
itemId,
|
|
144
|
+
attemptId: analysis.attemptId,
|
|
145
|
+
expectedRevision: analysis.revision,
|
|
146
|
+
evidenceId,
|
|
147
|
+
groups,
|
|
148
|
+
requestId: crypto.randomUUID(),
|
|
149
|
+
};
|
|
150
|
+
void run(() => host.split(input));
|
|
151
|
+
}
|
|
152
|
+
function locationLabel(location: EvidenceLocation) {
|
|
153
|
+
switch (location.kind) {
|
|
154
|
+
case 'page':
|
|
155
|
+
return t(M['ingestion.page'], { page: location.page });
|
|
156
|
+
case 'pages':
|
|
157
|
+
return t(M['ingestion.pages'], {
|
|
158
|
+
start: location.startPage,
|
|
159
|
+
end: location.endPage,
|
|
160
|
+
});
|
|
161
|
+
case 'time':
|
|
162
|
+
return t(M['ingestion.time'], {
|
|
163
|
+
start: location.startMs / 1000,
|
|
164
|
+
end: location.endMs / 1000,
|
|
165
|
+
});
|
|
166
|
+
default:
|
|
167
|
+
return t(M['ingestion.source']);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function locationUrl(evidenceId: string, location: EvidenceLocation) {
|
|
171
|
+
const evidence = view?.evidence.find(
|
|
172
|
+
(entry) => entry.evidence.id === evidenceId,
|
|
173
|
+
);
|
|
174
|
+
const url = evidenceUrl(evidence?.viewUrl);
|
|
175
|
+
if (!url) return undefined;
|
|
176
|
+
const base = url.split('#')[0];
|
|
177
|
+
if (
|
|
178
|
+
location.kind === 'page' &&
|
|
179
|
+
evidence?.evidence.mediaType === 'application/pdf'
|
|
180
|
+
)
|
|
181
|
+
return `${base}#page=${location.page}`;
|
|
182
|
+
if (
|
|
183
|
+
location.kind === 'pages' &&
|
|
184
|
+
evidence?.evidence.mediaType === 'application/pdf'
|
|
185
|
+
)
|
|
186
|
+
return `${base}#page=${location.startPage}`;
|
|
187
|
+
if (
|
|
188
|
+
location.kind === 'time' &&
|
|
189
|
+
evidence?.evidence.mediaType.startsWith('audio/')
|
|
190
|
+
)
|
|
191
|
+
return `${base}#t=${location.startMs / 1000},${location.endMs / 1000}`;
|
|
192
|
+
return url;
|
|
193
|
+
}
|
|
194
|
+
onMount(() => {
|
|
195
|
+
void load();
|
|
196
|
+
});
|
|
197
|
+
</script>
|
|
198
|
+
<section data-testid="intake-review" aria-busy={pending}>
|
|
199
|
+
<header><h1>{t(M['ingestion.review'])}</h1><Button onclick={() => load()} disabled={pending}>{t(M['ingestion.refresh'])}</Button></header>
|
|
200
|
+
{#if pending}<p role="status">{t(M['ingestion.loading'])}</p>{/if}
|
|
201
|
+
{#if error}<p role="alert">{t(M['ingestion.error'])}</p>{/if}
|
|
202
|
+
{#if view}
|
|
203
|
+
{#if view.availability !== 'available'}<p role="alert">{t(M[`ingestion.${view.availability}`])}</p>
|
|
204
|
+
{:else}
|
|
205
|
+
{@const extracted = view.analysis ? extractedContent(view.analysis.result.output) : []}
|
|
206
|
+
<h2>{view.entry.label}</h2><p>{t(M[`ingestion.${view.entry.state}`])}</p>
|
|
207
|
+
<div class="review-grid"><EvidenceViewer evidence={view.evidence} /><div class="details">
|
|
208
|
+
{#if extracted.length}
|
|
209
|
+
<h2>{t(M['ingestion.extracted'])}</h2>
|
|
210
|
+
{#each extracted as segment}
|
|
211
|
+
<article><pre>{segment.text}</pre>
|
|
212
|
+
{#if segment.confidence !== undefined}<p>{t(M['ingestion.confidence'])}: {segment.confidence}</p>{:else}<p>{t(M['ingestion.noConfidence'])}</p>{/if}
|
|
213
|
+
{#if segment.location}{@const url = locationUrl(segment.evidenceId,segment.location)}{#if url}<Button href={url} target="_blank" rel="noopener noreferrer">{locationLabel(segment.location)}</Button>{:else}<p>{locationLabel(segment.location)}</p>{/if}{/if}
|
|
214
|
+
</article>
|
|
215
|
+
{/each}
|
|
216
|
+
{/if}
|
|
217
|
+
{#if view.generation}
|
|
218
|
+
{@const generation = view.generation}
|
|
219
|
+
<h2>{t(M['ingestion.proposals'])}</h2><p>{generation.outcome}</p>
|
|
220
|
+
{#if typeof view.analysis?.result.confidence === 'number' && Number.isFinite(view.analysis.result.confidence)}<p>{t(M['ingestion.confidence'])}: {view.analysis.result.confidence}</p>{:else}<p>{t(M['ingestion.noConfidence'])}</p>{/if}
|
|
221
|
+
{#each generation.warnings as warning}<p>{warning}</p>{/each}
|
|
222
|
+
{#each generation.suggestions as suggestion,index}
|
|
223
|
+
<article><h3>{suggestion.handlerId}</h3><p>{suggestion.explanation}</p><pre>{JSON.stringify(suggestion.args,null,2)}</pre>
|
|
224
|
+
{#if suggestion.alternatives.length}<h4>{t(M['ingestion.alternatives'])}</h4><ul>{#each suggestion.alternatives as alternative}<li>{alternative}</li>{/each}</ul>{/if}
|
|
225
|
+
{#if suggestion.missingFields.length}<h4>{t(M['ingestion.missing'])}</h4><ul>{#each suggestion.missingFields as missing}<li>{missing}</li>{/each}</ul>{/if}
|
|
226
|
+
<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>
|
|
227
|
+
<Button disabled={suggestion.disposition !== 'ready_for_review' || !view.analysis} onclick={() => preview(index)}>{t(M['ingestion.preview'])}</Button>
|
|
228
|
+
</article>
|
|
229
|
+
{/each}
|
|
230
|
+
{#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}
|
|
231
|
+
{#if invalid}<p role="alert">{t(M['ingestion.invalid'])}</p>{/if}
|
|
232
|
+
{/if}
|
|
233
|
+
{#if !view.reviews.actions.length}<p>{t(M['ingestion.noReviews'])}</p>{/if}
|
|
234
|
+
{#each view.reviews.actions as action (`${action.review.actionId}:${action.review.revision}:${action.review.reviewVersion}`)}
|
|
235
|
+
<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} />
|
|
236
|
+
{/each}
|
|
237
|
+
{#if view.reviews.nextCursor}<Button onclick={() => load(loadedPages + 1)}>{t(M['ingestion.more'])}</Button>{/if}
|
|
238
|
+
</div></div>
|
|
239
|
+
{/if}{/if}
|
|
240
|
+
</section>
|
|
241
|
+
<style>
|
|
242
|
+
section{overflow-wrap:anywhere;max-width:90rem;margin-inline:auto;padding:1rem;color:var(--smrt-color-on-surface,#222)}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:1px solid var(--smrt-color-outline-variant,#ccc)}@media(max-width:48rem){.review-grid{grid-template-columns:minmax(0,1fr)}header{flex-wrap:wrap}}
|
|
243
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { IntakeReviewHost } from '../../review-dto.js';
|
|
2
|
+
export interface Props {
|
|
3
|
+
host: IntakeReviewHost;
|
|
4
|
+
itemId: string;
|
|
5
|
+
}
|
|
6
|
+
declare const ReviewView: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type ReviewView = ReturnType<typeof ReviewView>;
|
|
8
|
+
export default ReviewView;
|
|
9
|
+
//# sourceMappingURL=ReviewView.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReviewView.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ReviewView.svelte.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,gBAAgB,EAAkB,MAAM,qBAAqB,CAAC;AAM5E,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AAkPD,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export declare const M: {
|
|
2
|
+
readonly 'ingestion.planArgs': "ingestion.planArgs";
|
|
3
|
+
readonly 'ingestion.editPlan': "ingestion.editPlan";
|
|
4
|
+
readonly 'ingestion.inbox': "ingestion.inbox";
|
|
5
|
+
readonly 'ingestion.review': "ingestion.review";
|
|
6
|
+
readonly 'ingestion.loading': "ingestion.loading";
|
|
7
|
+
readonly 'ingestion.error': "ingestion.error";
|
|
8
|
+
readonly 'ingestion.refresh': "ingestion.refresh";
|
|
9
|
+
readonly 'ingestion.empty': "ingestion.empty";
|
|
10
|
+
readonly 'ingestion.all': "ingestion.all";
|
|
11
|
+
readonly 'ingestion.filter': "ingestion.filter";
|
|
12
|
+
readonly 'ingestion.assignee': "ingestion.assignee";
|
|
13
|
+
readonly 'ingestion.assign': "ingestion.assign";
|
|
14
|
+
readonly 'ingestion.unassigned': "ingestion.unassigned";
|
|
15
|
+
readonly 'ingestion.upload': "ingestion.upload";
|
|
16
|
+
readonly 'ingestion.files': "ingestion.files";
|
|
17
|
+
readonly 'ingestion.filesHelp': "ingestion.filesHelp";
|
|
18
|
+
readonly 'ingestion.more': "ingestion.more";
|
|
19
|
+
readonly 'ingestion.open': "ingestion.open";
|
|
20
|
+
readonly 'ingestion.processing': "ingestion.processing";
|
|
21
|
+
readonly 'ingestion.unresolved': "ingestion.unresolved";
|
|
22
|
+
readonly 'ingestion.waiting': "ingestion.waiting";
|
|
23
|
+
readonly 'ingestion.failed': "ingestion.failed";
|
|
24
|
+
readonly 'ingestion.deferred': "ingestion.deferred";
|
|
25
|
+
readonly 'ingestion.partially_completed': "ingestion.partially_completed";
|
|
26
|
+
readonly 'ingestion.completed': "ingestion.completed";
|
|
27
|
+
readonly 'ingestion.evidence': "ingestion.evidence";
|
|
28
|
+
readonly 'ingestion.original': "ingestion.original";
|
|
29
|
+
readonly 'ingestion.noOriginal': "ingestion.noOriginal";
|
|
30
|
+
readonly 'ingestion.extracted': "ingestion.extracted";
|
|
31
|
+
readonly 'ingestion.confidence': "ingestion.confidence";
|
|
32
|
+
readonly 'ingestion.noConfidence': "ingestion.noConfidence";
|
|
33
|
+
readonly 'ingestion.proposals': "ingestion.proposals";
|
|
34
|
+
readonly 'ingestion.preview': "ingestion.preview";
|
|
35
|
+
readonly 'ingestion.args': "ingestion.args";
|
|
36
|
+
readonly 'ingestion.effects': "ingestion.effects";
|
|
37
|
+
readonly 'ingestion.approve': "ingestion.approve";
|
|
38
|
+
readonly 'ingestion.reject': "ingestion.reject";
|
|
39
|
+
readonly 'ingestion.defer': "ingestion.defer";
|
|
40
|
+
readonly 'ingestion.correct': "ingestion.correct";
|
|
41
|
+
readonly 'ingestion.apply': "ingestion.apply";
|
|
42
|
+
readonly 'ingestion.invalid': "ingestion.invalid";
|
|
43
|
+
readonly 'ingestion.alternatives': "ingestion.alternatives";
|
|
44
|
+
readonly 'ingestion.missing': "ingestion.missing";
|
|
45
|
+
readonly 'ingestion.candidates': "ingestion.candidates";
|
|
46
|
+
readonly 'ingestion.search': "ingestion.search";
|
|
47
|
+
readonly 'ingestion.query': "ingestion.query";
|
|
48
|
+
readonly 'ingestion.field': "ingestion.field";
|
|
49
|
+
readonly 'ingestion.choose': "ingestion.choose";
|
|
50
|
+
readonly 'ingestion.split': "ingestion.split";
|
|
51
|
+
readonly 'ingestion.saveSplit': "ingestion.saveSplit";
|
|
52
|
+
readonly 'ingestion.result': "ingestion.result";
|
|
53
|
+
readonly 'ingestion.stale': "ingestion.stale";
|
|
54
|
+
readonly 'ingestion.revoked': "ingestion.revoked";
|
|
55
|
+
readonly 'ingestion.expired': "ingestion.expired";
|
|
56
|
+
readonly 'ingestion.unavailable': "ingestion.unavailable";
|
|
57
|
+
readonly 'ingestion.feedback': "ingestion.feedback";
|
|
58
|
+
readonly 'ingestion.correctJudgment': "ingestion.correctJudgment";
|
|
59
|
+
readonly 'ingestion.incorrectJudgment': "ingestion.incorrectJudgment";
|
|
60
|
+
readonly 'ingestion.comment': "ingestion.comment";
|
|
61
|
+
readonly 'ingestion.page': "ingestion.page";
|
|
62
|
+
readonly 'ingestion.pages': "ingestion.pages";
|
|
63
|
+
readonly 'ingestion.time': "ingestion.time";
|
|
64
|
+
readonly 'ingestion.source': "ingestion.source";
|
|
65
|
+
readonly 'ingestion.truncated': "ingestion.truncated";
|
|
66
|
+
readonly 'ingestion.reviewAgain': "ingestion.reviewAgain";
|
|
67
|
+
readonly 'ingestion.noReviews': "ingestion.noReviews";
|
|
68
|
+
readonly 'ingestion.state': "ingestion.state";
|
|
69
|
+
readonly 'ingestion.reason': "ingestion.reason";
|
|
70
|
+
readonly 'ingestion.feedbackSent': "ingestion.feedbackSent";
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=i18n.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../src/svelte/i18n.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuEZ,CAAC"}
|