@semiont/react-ui 0.5.23 → 0.5.25
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/dist/{PdfAnnotationCanvas.client-V7Q3BEEQ.js → PdfAnnotationCanvas.client-EHQWF6BR.js} +45 -10
- package/dist/PdfAnnotationCanvas.client-EHQWF6BR.js.map +1 -0
- package/dist/{ar-JRTKCMTF.js → ar-CULRHLNG.js} +20 -6
- package/dist/ar-CULRHLNG.js.map +1 -0
- package/dist/{bn-LO6EDSCC.js → bn-HYR7CEL2.js} +20 -6
- package/dist/bn-HYR7CEL2.js.map +1 -0
- package/dist/{chunk-5Y5JDCKC.js → chunk-NH6L2GIL.js} +20 -6
- package/dist/chunk-NH6L2GIL.js.map +1 -0
- package/dist/{chunk-RO5CRVIA.js → chunk-TG2XQEC4.js} +92 -60
- package/dist/chunk-TG2XQEC4.js.map +1 -0
- package/dist/{cs-LCHHOR4O.js → cs-YBRWY7NH.js} +20 -6
- package/dist/cs-YBRWY7NH.js.map +1 -0
- package/dist/{da-TARGPMJR.js → da-PHDEOBV3.js} +20 -6
- package/dist/da-PHDEOBV3.js.map +1 -0
- package/dist/{de-SBH4QBPJ.js → de-O4UCCVLJ.js} +20 -6
- package/dist/de-O4UCCVLJ.js.map +1 -0
- package/dist/{el-2D2OLGWB.js → el-MVGO6WZV.js} +20 -6
- package/dist/el-MVGO6WZV.js.map +1 -0
- package/dist/{en-QFIEG3CA.js → en-2KQILFKQ.js} +4 -2
- package/dist/{es-U6YK2LX6.js → es-USSLXQY7.js} +20 -6
- package/dist/es-USSLXQY7.js.map +1 -0
- package/dist/{fa-S7SZE37M.js → fa-WMAREFWY.js} +20 -6
- package/dist/fa-WMAREFWY.js.map +1 -0
- package/dist/{fi-CBL3NIXN.js → fi-B3NBNOTS.js} +20 -6
- package/dist/fi-B3NBNOTS.js.map +1 -0
- package/dist/{fr-EABUHV6S.js → fr-TXYK5T5W.js} +20 -6
- package/dist/fr-TXYK5T5W.js.map +1 -0
- package/dist/{he-THWD244Y.js → he-JCJACW5D.js} +20 -6
- package/dist/he-JCJACW5D.js.map +1 -0
- package/dist/{hi-GHGFZJ44.js → hi-VOQLZNJU.js} +20 -6
- package/dist/hi-VOQLZNJU.js.map +1 -0
- package/dist/{id-POZOUMDW.js → id-BFN5RZNZ.js} +20 -6
- package/dist/id-BFN5RZNZ.js.map +1 -0
- package/dist/index.css +0 -36
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +102 -53
- package/dist/index.js +2132 -2097
- package/dist/index.js.map +1 -1
- package/dist/{it-KDYJADSH.js → it-TWWDHKKM.js} +20 -6
- package/dist/it-TWWDHKKM.js.map +1 -0
- package/dist/{ja-Z5Z6ZF3P.js → ja-YRQLEBFM.js} +20 -6
- package/dist/ja-YRQLEBFM.js.map +1 -0
- package/dist/{ko-7X3XUXRN.js → ko-PG6B3CRB.js} +20 -6
- package/dist/ko-PG6B3CRB.js.map +1 -0
- package/dist/{ms-BTWEIW43.js → ms-7EZECVKM.js} +20 -6
- package/dist/ms-7EZECVKM.js.map +1 -0
- package/dist/{nl-IS2CGF5I.js → nl-HP753YAP.js} +20 -6
- package/dist/nl-HP753YAP.js.map +1 -0
- package/dist/{no-AAMEVA26.js → no-JUD5WX3N.js} +20 -6
- package/dist/no-JUD5WX3N.js.map +1 -0
- package/dist/{pl-PVXUQYFT.js → pl-RVUL4CBM.js} +20 -6
- package/dist/pl-RVUL4CBM.js.map +1 -0
- package/dist/{pt-3ITQ76TL.js → pt-RPOVD4QM.js} +20 -6
- package/dist/pt-RPOVD4QM.js.map +1 -0
- package/dist/{ro-FRB4IIVS.js → ro-DMYWQTRD.js} +20 -6
- package/dist/ro-DMYWQTRD.js.map +1 -0
- package/dist/{sv-DS3RO24M.js → sv-DMZ4FGXD.js} +20 -6
- package/dist/sv-DMZ4FGXD.js.map +1 -0
- package/dist/test-utils.js +4 -3
- package/dist/test-utils.js.map +1 -1
- package/dist/{th-AX66MWWB.js → th-4HVBJBQ2.js} +20 -6
- package/dist/th-4HVBJBQ2.js.map +1 -0
- package/dist/{tr-7KTB67TT.js → tr-6BDPU5ML.js} +20 -6
- package/dist/tr-6BDPU5ML.js.map +1 -0
- package/dist/{uk-SMQS2MAQ.js → uk-TBZBT2TL.js} +20 -6
- package/dist/uk-TBZBT2TL.js.map +1 -0
- package/dist/{vi-D3JXL2EZ.js → vi-UMKZKZL3.js} +20 -6
- package/dist/vi-UMKZKZL3.js.map +1 -0
- package/dist/{zh-S754AWFS.js → zh-YBJWJJYB.js} +20 -6
- package/dist/zh-YBJWJJYB.js.map +1 -0
- package/package.json +8 -8
- package/src/components/AssistProgress.tsx +42 -30
- package/src/components/EntityFoundLog.tsx +38 -0
- package/src/components/__tests__/AssistProgress.test.tsx +86 -28
- package/src/components/annotation-popups/JsonLdView.tsx +1 -1
- package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +8 -2
- package/src/components/modals/ReferenceWizardModal.tsx +2 -1
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +112 -12
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +247 -1
- package/src/components/resource/AnnotateView.tsx +14 -7
- package/src/components/resource/BrowseView.tsx +2 -7
- package/src/components/resource/DownloadFileLink.tsx +49 -0
- package/src/components/resource/__tests__/AnnotateView.focus.test.tsx +58 -0
- package/src/components/resource/__tests__/download-link.test.tsx +129 -0
- package/src/components/resource/panels/AssistSection.css +0 -43
- package/src/components/resource/panels/AssistSection.tsx +11 -2
- package/src/components/resource/panels/AssistShell.tsx +5 -1
- package/src/components/resource/panels/JsonLdPanel.tsx +1 -1
- package/src/components/resource/panels/ReferencesPanel.tsx +12 -13
- package/src/components/resource/panels/TaggingPanel.tsx +21 -1
- package/src/components/resource/panels/__tests__/AssistSection.test.tsx +2 -2
- package/src/components/resource/panels/__tests__/AssistShell.test.tsx +9 -4
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +1 -1
- package/src/components/resource/panels/__tests__/JsonLdPanel.test.tsx +8 -2
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +26 -0
- package/src/components/settings/SettingsPanel.tsx +6 -2
- package/src/components/settings/__tests__/SettingsPanel.test.tsx +28 -3
- package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +3 -11
- package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -3
- package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +0 -1
- package/src/features/admin-exchange/components/AdminExchangePage.tsx +0 -3
- package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +1 -1
- package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -8
- package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -3
- package/src/features/admin-security/state/__tests__/admin-security-state-unit.test.ts +1 -1
- package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -1
- package/src/features/admin-users/components/AdminUsersPage.tsx +0 -3
- package/src/features/admin-users/state/__tests__/admin-users-state-unit.test.ts +1 -1
- package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +1 -1
- package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -2
- package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +0 -3
- package/src/features/moderate-entity-tags/state/__tests__/entity-tags-state-unit.test.ts +1 -1
- package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +3 -5
- package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +0 -3
- package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +3 -5
- package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +0 -3
- package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +0 -1
- package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +0 -3
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +17 -7
- package/src/features/resource-compose/components/ResourceComposePage.tsx +5 -3
- package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +1 -1
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +0 -1
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +0 -3
- package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +1 -1
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +40 -8
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +20 -16
- package/src/features/resource-viewer/state/__tests__/resource-loader-state-unit.test.ts +1 -1
- package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +1 -1
- package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +2 -2
- package/translations/ar.json +17 -5
- package/translations/bn.json +17 -5
- package/translations/cs.json +17 -5
- package/translations/da.json +17 -5
- package/translations/de.json +17 -5
- package/translations/el.json +17 -5
- package/translations/en.json +17 -5
- package/translations/es.json +17 -5
- package/translations/fa.json +17 -5
- package/translations/fi.json +17 -5
- package/translations/fr.json +17 -5
- package/translations/he.json +17 -5
- package/translations/hi.json +17 -5
- package/translations/id.json +17 -5
- package/translations/it.json +17 -5
- package/translations/ja.json +17 -5
- package/translations/ko.json +17 -5
- package/translations/ms.json +17 -5
- package/translations/nl.json +17 -5
- package/translations/no.json +17 -5
- package/translations/pl.json +17 -5
- package/translations/pt.json +17 -5
- package/translations/ro.json +17 -5
- package/translations/sv.json +17 -5
- package/translations/th.json +17 -5
- package/translations/tr.json +17 -5
- package/translations/uk.json +17 -5
- package/translations/vi.json +17 -5
- package/translations/zh.json +17 -5
- package/dist/PdfAnnotationCanvas.client-V7Q3BEEQ.js.map +0 -1
- package/dist/ar-JRTKCMTF.js.map +0 -1
- package/dist/bn-LO6EDSCC.js.map +0 -1
- package/dist/chunk-5Y5JDCKC.js.map +0 -1
- package/dist/chunk-RO5CRVIA.js.map +0 -1
- package/dist/cs-LCHHOR4O.js.map +0 -1
- package/dist/da-TARGPMJR.js.map +0 -1
- package/dist/de-SBH4QBPJ.js.map +0 -1
- package/dist/el-2D2OLGWB.js.map +0 -1
- package/dist/es-U6YK2LX6.js.map +0 -1
- package/dist/fa-S7SZE37M.js.map +0 -1
- package/dist/fi-CBL3NIXN.js.map +0 -1
- package/dist/fr-EABUHV6S.js.map +0 -1
- package/dist/he-THWD244Y.js.map +0 -1
- package/dist/hi-GHGFZJ44.js.map +0 -1
- package/dist/id-POZOUMDW.js.map +0 -1
- package/dist/it-KDYJADSH.js.map +0 -1
- package/dist/ja-Z5Z6ZF3P.js.map +0 -1
- package/dist/ko-7X3XUXRN.js.map +0 -1
- package/dist/ms-BTWEIW43.js.map +0 -1
- package/dist/nl-IS2CGF5I.js.map +0 -1
- package/dist/no-AAMEVA26.js.map +0 -1
- package/dist/pl-PVXUQYFT.js.map +0 -1
- package/dist/pt-3ITQ76TL.js.map +0 -1
- package/dist/ro-FRB4IIVS.js.map +0 -1
- package/dist/sv-DS3RO24M.js.map +0 -1
- package/dist/th-AX66MWWB.js.map +0 -1
- package/dist/tr-7KTB67TT.js.map +0 -1
- package/dist/uk-SMQS2MAQ.js.map +0 -1
- package/dist/vi-D3JXL2EZ.js.map +0 -1
- package/dist/zh-S754AWFS.js.map +0 -1
- /package/dist/{en-QFIEG3CA.js.map → en-2KQILFKQ.js.map} +0 -0
|
@@ -7,13 +7,19 @@
|
|
|
7
7
|
* cancel/dismiss arrive as callbacks the caller wires (job.cancelRequest /
|
|
8
8
|
* mark.dismissProgress). Feature blocks are data-presence-driven so each call
|
|
9
9
|
* site keeps its current visuals by passing what it always had.
|
|
10
|
+
*
|
|
11
|
+
* i18n contract (ASSIST-SURFACE-WARTS Lane A): every string this component
|
|
12
|
+
* renders comes from `translations`. There are NO English fallbacks — a
|
|
13
|
+
* missing key must be a type error at the call site, not a silent English
|
|
14
|
+
* leak in a Japanese UI. Tests use key-echo strings ('tr.complete') so an
|
|
15
|
+
* assertion failing means "rendered something other than what was passed".
|
|
10
16
|
*/
|
|
11
17
|
import { describe, it, expect, vi } from 'vitest';
|
|
12
18
|
import { render, screen } from '@testing-library/react';
|
|
13
19
|
import userEvent from '@testing-library/user-event';
|
|
14
20
|
import '@testing-library/jest-dom';
|
|
15
21
|
import type { components } from '@semiont/core';
|
|
16
|
-
import { AssistProgress } from '../AssistProgress';
|
|
22
|
+
import { AssistProgress, type AssistProgressTranslations } from '../AssistProgress';
|
|
17
23
|
|
|
18
24
|
type JobProgress = components['schemas']['JobProgress'];
|
|
19
25
|
|
|
@@ -21,13 +27,26 @@ const running = (over: Partial<JobProgress> = {}): JobProgress => ({
|
|
|
21
27
|
stage: 'analyzing', percentage: 40, message: 'working on it', ...over,
|
|
22
28
|
});
|
|
23
29
|
|
|
30
|
+
/** Every required key, echoing its own name; override per test. */
|
|
31
|
+
const T = (over: Partial<AssistProgressTranslations> = {}): AssistProgressTranslations => ({
|
|
32
|
+
cancel: 'tr.cancel',
|
|
33
|
+
inProgress: 'tr.inProgress',
|
|
34
|
+
complete: 'tr.complete',
|
|
35
|
+
failed: 'tr.failed',
|
|
36
|
+
close: 'tr.close',
|
|
37
|
+
paramsTitle: 'tr.paramsTitle',
|
|
38
|
+
processing: (label: string) => `tr.processing(${label})`,
|
|
39
|
+
...over,
|
|
40
|
+
});
|
|
41
|
+
|
|
24
42
|
describe('AssistProgress', () => {
|
|
25
43
|
it('renders provider-free: message, params, and data hooks', () => {
|
|
26
44
|
const { container } = render(
|
|
27
45
|
<AssistProgress
|
|
28
46
|
progress={running({ requestParams: [{ label: 'Density', value: '5' }] })}
|
|
29
47
|
dataType="comment"
|
|
30
|
-
|
|
48
|
+
translations={T()}
|
|
49
|
+
/>,
|
|
31
50
|
);
|
|
32
51
|
expect(screen.getByText('working on it')).toBeInTheDocument();
|
|
33
52
|
expect(screen.getByText(/Density/)).toBeInTheDocument();
|
|
@@ -37,12 +56,24 @@ describe('AssistProgress', () => {
|
|
|
37
56
|
expect(root).toHaveAttribute('data-status', 'analyzing');
|
|
38
57
|
});
|
|
39
58
|
|
|
59
|
+
it('renders the request-parameters title from translations, never a literal', () => {
|
|
60
|
+
render(
|
|
61
|
+
<AssistProgress
|
|
62
|
+
progress={running({ requestParams: [{ label: 'Density', value: '5' }] })}
|
|
63
|
+
dataType="comment"
|
|
64
|
+
translations={T()}
|
|
65
|
+
/>,
|
|
66
|
+
);
|
|
67
|
+
expect(screen.getByText('tr.paramsTitle')).toBeInTheDocument();
|
|
68
|
+
expect(screen.queryByText('Request Parameters:')).not.toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
|
|
40
71
|
it('renders a title header only when given one', () => {
|
|
41
72
|
const { rerender } = render(
|
|
42
|
-
<AssistProgress progress={running()} dataType="reference" translations={{ title: 'Annotating Entity References' }} />,
|
|
73
|
+
<AssistProgress progress={running()} dataType="reference" translations={T({ title: 'Annotating Entity References' })} />,
|
|
43
74
|
);
|
|
44
75
|
expect(screen.getByText('Annotating Entity References')).toBeInTheDocument();
|
|
45
|
-
rerender(<AssistProgress progress={running()} dataType="comment" />);
|
|
76
|
+
rerender(<AssistProgress progress={running()} dataType="comment" translations={T()} />);
|
|
46
77
|
expect(screen.queryByText('Annotating Entity References')).not.toBeInTheDocument();
|
|
47
78
|
});
|
|
48
79
|
|
|
@@ -50,7 +81,7 @@ describe('AssistProgress', () => {
|
|
|
50
81
|
// Cancel lives in the title header — both flows that offer cancel
|
|
51
82
|
// (reference detection, generation) render the titled profile.
|
|
52
83
|
const onCancel = vi.fn();
|
|
53
|
-
const tr = { title: 'Generating Resource', cancel: 'Cancel Job' };
|
|
84
|
+
const tr = T({ title: 'Generating Resource', cancel: 'Cancel Job' });
|
|
54
85
|
const { rerender } = render(
|
|
55
86
|
<AssistProgress progress={running()} dataType="generation" onCancel={onCancel} translations={tr} />,
|
|
56
87
|
);
|
|
@@ -68,13 +99,20 @@ describe('AssistProgress', () => {
|
|
|
68
99
|
expect(screen.queryByTitle('Cancel Job')).not.toBeInTheDocument();
|
|
69
100
|
});
|
|
70
101
|
|
|
71
|
-
it('stage branching:
|
|
102
|
+
it('stage branching: both terminal stages prefer the job message, then the translated copy', () => {
|
|
103
|
+
// The job composes detail no static string can ("Created 14 highlights"),
|
|
104
|
+
// so it wins on BOTH terminal stages; the translated copy covers the
|
|
105
|
+
// required-but-empty message case (pinned below).
|
|
72
106
|
const { rerender } = render(
|
|
73
|
-
<AssistProgress progress={running({ stage: 'complete' })} dataType="reference" translations={{ complete: 'All done!' }} />,
|
|
107
|
+
<AssistProgress progress={running({ stage: 'complete' })} dataType="reference" translations={T({ complete: 'All done!' })} />,
|
|
108
|
+
);
|
|
109
|
+
expect(screen.getByText('working on it')).toBeInTheDocument();
|
|
110
|
+
rerender(
|
|
111
|
+
<AssistProgress progress={running({ stage: 'complete', message: '' })} dataType="reference" translations={T({ complete: 'All done!' })} />,
|
|
74
112
|
);
|
|
75
113
|
expect(screen.getByText('All done!')).toBeInTheDocument();
|
|
76
114
|
rerender(
|
|
77
|
-
<AssistProgress progress={running({ stage: 'error', message: 'it broke' })} dataType="reference" translations={{ failed: 'Failed' }} />,
|
|
115
|
+
<AssistProgress progress={running({ stage: 'error', message: 'it broke' })} dataType="reference" translations={T({ failed: 'Failed' })} />,
|
|
78
116
|
);
|
|
79
117
|
expect(screen.getByText('it broke')).toBeInTheDocument();
|
|
80
118
|
});
|
|
@@ -83,7 +121,7 @@ describe('AssistProgress', () => {
|
|
|
83
121
|
render(
|
|
84
122
|
<AssistProgress
|
|
85
123
|
progress={running({ completedEntityTypes: [{ entityType: 'Person', foundCount: 3 }] })}
|
|
86
|
-
dataType="reference" translations={{ found: (n) => `Found ${n}` }}
|
|
124
|
+
dataType="reference" translations={T({ found: (n) => `Found ${n}` })}
|
|
87
125
|
/>,
|
|
88
126
|
);
|
|
89
127
|
expect(screen.getByText('Person:')).toBeInTheDocument();
|
|
@@ -94,55 +132,75 @@ describe('AssistProgress', () => {
|
|
|
94
132
|
const { rerender } = render(
|
|
95
133
|
<AssistProgress
|
|
96
134
|
progress={running({ currentEntityType: 'Location' })}
|
|
97
|
-
dataType="reference" translations={{ current: (l) => `
|
|
135
|
+
dataType="reference" translations={T({ current: (l) => `Current: ${l}` })}
|
|
98
136
|
/>,
|
|
99
137
|
);
|
|
100
|
-
expect(screen.getByText('
|
|
138
|
+
expect(screen.getByText('Current: Location')).toBeInTheDocument();
|
|
101
139
|
rerender(
|
|
102
140
|
<AssistProgress
|
|
103
141
|
progress={running({ currentCategory: 'Rule', processedCategories: 2, totalCategories: 5 })}
|
|
104
|
-
dataType="tag"
|
|
142
|
+
dataType="tag" translations={T()}
|
|
143
|
+
/>,
|
|
105
144
|
);
|
|
106
145
|
expect(screen.getByText(/Rule/)).toBeInTheDocument();
|
|
107
146
|
expect(screen.getByText(/2\/5/)).toBeInTheDocument();
|
|
108
147
|
});
|
|
109
148
|
|
|
149
|
+
it('falls back to the generic processing formatter — never the English literal', () => {
|
|
150
|
+
// The reference flow passes its own `current` wording; every other flow
|
|
151
|
+
// (tag categories, and any flow without one) uses `processing`. Neither
|
|
152
|
+
// path may render a hardcoded "Processing: ".
|
|
153
|
+
const { rerender } = render(
|
|
154
|
+
<AssistProgress progress={running({ currentEntityType: 'Location' })} dataType="reference" translations={T()} />,
|
|
155
|
+
);
|
|
156
|
+
expect(screen.getByText('tr.processing(Location)')).toBeInTheDocument();
|
|
157
|
+
rerender(
|
|
158
|
+
<AssistProgress progress={running({ currentCategory: 'Rule' })} dataType="tag" translations={T()} />,
|
|
159
|
+
);
|
|
160
|
+
expect(screen.getByText(/tr\.processing\(Rule\)/)).toBeInTheDocument();
|
|
161
|
+
expect(screen.queryByText(/^Processing: /)).not.toBeInTheDocument();
|
|
162
|
+
});
|
|
163
|
+
|
|
110
164
|
it('renders the percentage bar only when opted in', () => {
|
|
111
165
|
const { container, rerender } = render(
|
|
112
|
-
<AssistProgress progress={running({ percentage: 40 })} dataType="tag" showPercentBar />,
|
|
166
|
+
<AssistProgress progress={running({ percentage: 40 })} dataType="tag" showPercentBar translations={T()} />,
|
|
113
167
|
);
|
|
114
168
|
const fill = container.querySelector('.semiont-progress-bar__fill');
|
|
115
169
|
expect(fill).toBeInTheDocument();
|
|
116
170
|
expect(fill).toHaveStyle({ width: '40%' });
|
|
117
171
|
rerender(
|
|
118
|
-
<AssistProgress progress={running({ percentage: 40 })} dataType="reference" />,
|
|
172
|
+
<AssistProgress progress={running({ percentage: 40 })} dataType="reference" translations={T()} />,
|
|
119
173
|
);
|
|
120
174
|
expect(container.querySelector('.semiont-progress-bar__fill')).not.toBeInTheDocument();
|
|
121
175
|
});
|
|
122
176
|
|
|
123
|
-
it('cancel and dismiss
|
|
124
|
-
// ✕ / × glyphs alone are meaningless to screen readers
|
|
125
|
-
//
|
|
177
|
+
it('cancel and dismiss take their accessible names from translations', () => {
|
|
178
|
+
// ✕ / × glyphs alone are meaningless to screen readers, and an English
|
|
179
|
+
// fallback is meaningless to a non-English reader. The labels are
|
|
180
|
+
// required keys, so there is nothing to fall back TO.
|
|
126
181
|
render(
|
|
127
182
|
<AssistProgress progress={running()} dataType="generation"
|
|
128
183
|
onCancel={vi.fn()} onDismiss={vi.fn()}
|
|
129
|
-
translations={{ title: 'Generating' }} />,
|
|
184
|
+
translations={T({ title: 'Generating' })} />,
|
|
130
185
|
);
|
|
131
|
-
expect(screen.getByLabelText('
|
|
132
|
-
expect(screen.getByLabelText('
|
|
186
|
+
expect(screen.getByLabelText('tr.cancel')).toBeInTheDocument();
|
|
187
|
+
expect(screen.getByLabelText('tr.close')).toBeInTheDocument();
|
|
188
|
+
expect(screen.queryByLabelText('Cancel')).not.toBeInTheDocument();
|
|
189
|
+
expect(screen.queryByLabelText('Dismiss')).not.toBeInTheDocument();
|
|
133
190
|
});
|
|
134
191
|
|
|
135
|
-
it('terminal stages never render a blank status line', () => {
|
|
192
|
+
it('terminal stages never render a blank status line, and never an English one', () => {
|
|
136
193
|
// JobProgress.message is required but can be '' — a terminal display with
|
|
137
|
-
// no text at all is worse than a generic word
|
|
194
|
+
// no text at all is worse than a generic word, but that word must be the
|
|
195
|
+
// caller's translated one.
|
|
138
196
|
const { rerender } = render(
|
|
139
|
-
<AssistProgress progress={running({ stage: 'complete', message: '' })} dataType="reference" />,
|
|
197
|
+
<AssistProgress progress={running({ stage: 'complete', message: '' })} dataType="reference" translations={T()} />,
|
|
140
198
|
);
|
|
141
|
-
expect(screen.getByText('
|
|
199
|
+
expect(screen.getByText('tr.complete')).toBeInTheDocument();
|
|
142
200
|
rerender(
|
|
143
|
-
<AssistProgress progress={running({ stage: 'error', message: '' })} dataType="reference" />,
|
|
201
|
+
<AssistProgress progress={running({ stage: 'error', message: '' })} dataType="reference" translations={T()} />,
|
|
144
202
|
);
|
|
145
|
-
expect(screen.getByText('
|
|
203
|
+
expect(screen.getByText('tr.failed')).toBeInTheDocument();
|
|
146
204
|
});
|
|
147
205
|
|
|
148
206
|
it('renders dismiss whenever the caller offers it (WHEN is the caller\'s policy)', async () => {
|
|
@@ -151,12 +209,12 @@ describe('AssistProgress', () => {
|
|
|
151
209
|
// "callback present → affordance rendered".
|
|
152
210
|
const onDismiss = vi.fn();
|
|
153
211
|
const { rerender } = render(
|
|
154
|
-
<AssistProgress progress={running()} dataType="highlight" translations={{ close: 'Close' }} />,
|
|
212
|
+
<AssistProgress progress={running()} dataType="highlight" translations={T({ close: 'Close' })} />,
|
|
155
213
|
);
|
|
156
214
|
expect(screen.queryByLabelText('Close')).not.toBeInTheDocument();
|
|
157
215
|
rerender(
|
|
158
216
|
<AssistProgress progress={running()} dataType="highlight"
|
|
159
|
-
onDismiss={onDismiss} translations={{ close: 'Close' }} />,
|
|
217
|
+
onDismiss={onDismiss} translations={T({ close: 'Close' })} />,
|
|
160
218
|
);
|
|
161
219
|
await userEvent.click(screen.getByLabelText('Close'));
|
|
162
220
|
expect(onDismiss).toHaveBeenCalledOnce();
|
|
@@ -7,7 +7,7 @@ import { json } from '@codemirror/lang-json';
|
|
|
7
7
|
import { oneDark } from '@codemirror/theme-one-dark';
|
|
8
8
|
import { syntaxHighlighting } from '@codemirror/language';
|
|
9
9
|
import { jsonLightTheme, jsonLightHighlightStyle } from '../../lib/codemirror-json-theme';
|
|
10
|
-
import { useLineNumbers } from '../../
|
|
10
|
+
import { useLineNumbers } from '../../contexts/LineNumbersContext';
|
|
11
11
|
|
|
12
12
|
import type { Annotation } from '@semiont/core';
|
|
13
13
|
|
|
@@ -47,9 +47,15 @@ vi.mock('../../../lib/codemirror-json-theme', () => ({
|
|
|
47
47
|
jsonLightHighlightStyle: {},
|
|
48
48
|
}));
|
|
49
49
|
|
|
50
|
-
vi.mock('@/
|
|
50
|
+
vi.mock('@/contexts/LineNumbersContext', async () => {
|
|
51
|
+
const actual = await vi.importActual('@/contexts/LineNumbersContext');
|
|
52
|
+
// Mock ONLY the hook: renderWithProviders mounts the real
|
|
53
|
+
// LineNumbersProvider, which an auto-mock would swallow (rendering
|
|
54
|
+
// nothing), while these tests want a fixed showLineNumbers value.
|
|
55
|
+
return { ...actual, useLineNumbers: vi.fn() };
|
|
56
|
+
});
|
|
51
57
|
|
|
52
|
-
import { useLineNumbers } from '@/
|
|
58
|
+
import { useLineNumbers } from '@/contexts/LineNumbersContext';
|
|
53
59
|
import { renderWithProviders } from '../../../test-utils';
|
|
54
60
|
import { JsonLdView } from '../JsonLdView';
|
|
55
61
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useState, useEffect, useCallback } from 'react';
|
|
4
4
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
|
5
5
|
import type { GatheredContext } from '@semiont/core';
|
|
6
|
+
import { uuidV4 } from '@semiont/core';
|
|
6
7
|
import { useSemiont } from '../../session/SemiontProvider';
|
|
7
8
|
import { useObservable } from '../../hooks/useObservable';
|
|
8
9
|
import { useEventSubscription } from '../../contexts/useEventSubscription';
|
|
@@ -144,7 +145,7 @@ export function ReferenceWizardModal({
|
|
|
144
145
|
setIsSearching(true);
|
|
145
146
|
const contextWithHint = userHint ? { ...context, userHint } : context;
|
|
146
147
|
session?.client.match.requestSearch({
|
|
147
|
-
correlationId:
|
|
148
|
+
correlationId: uuidV4(),
|
|
148
149
|
resourceId,
|
|
149
150
|
referenceId: annotationId,
|
|
150
151
|
context: contextWithHint,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React, { useRef, useState, useCallback, useEffect, useMemo } from 'react';
|
|
4
|
-
import type { Annotation, AnchorRect } from '@semiont/core';
|
|
4
|
+
import type { Annotation, AnchorRect, AnchoredText } from '@semiont/core';
|
|
5
5
|
import { resourceId as toResourceId } from '@semiont/core';
|
|
6
6
|
import { toViewportAnchorRect } from '../../lib/anchor-rect';
|
|
7
|
-
import { createFragmentSelector } from '@semiont/core';
|
|
7
|
+
import { createFragmentSelector, anchorRuns, isTextRun, textUnder } from '@semiont/core';
|
|
8
8
|
import { rectsForPage } from './rects-for-page';
|
|
9
9
|
import { createHoverHandlers, type SemiontSession } from '@semiont/sdk';
|
|
10
10
|
import type { SelectionMotivation } from '../annotation/AnnotateToolbar';
|
|
@@ -61,7 +61,7 @@ interface PdfAnnotationCanvasProps {
|
|
|
61
61
|
* PDF annotation canvas with page navigation and rectangle drawing
|
|
62
62
|
*
|
|
63
63
|
* @emits browse:click - Annotation clicked on PDF. Payload: { annotationId: string, motivation: Motivation }
|
|
64
|
-
* @emits mark:requested - New annotation drawn on PDF. Payload: { selector: FragmentSelector, motivation: SelectionMotivation }
|
|
64
|
+
* @emits mark:requested - New annotation drawn on PDF. Payload: { selector: [FragmentSelector, TextQuoteSelector?], motivation: SelectionMotivation } — the quote is the text under the rectangle, omitted when the page has no text layer
|
|
65
65
|
* @emits beckon:hover - Annotation hovered or unhovered. Payload: { annotationId: string | null }
|
|
66
66
|
*/
|
|
67
67
|
export function PdfAnnotationCanvas({
|
|
@@ -83,6 +83,13 @@ export function PdfAnnotationCanvas({
|
|
|
83
83
|
const [isLoading, setIsLoading] = useState(true);
|
|
84
84
|
const [error, setError] = useState<string | null>(null);
|
|
85
85
|
const [pageDimensions, setPageDimensions] = useState<{ width: number; height: number } | null>(null);
|
|
86
|
+
/**
|
|
87
|
+
* The current page's text and per-run geometry, read once when the page
|
|
88
|
+
* loads so a drag can be quoted without a round trip. Null while the page
|
|
89
|
+
* is loading; `items` is empty on a scanned page, which has no text layer
|
|
90
|
+
* for the browser to read.
|
|
91
|
+
*/
|
|
92
|
+
const [pageAnchored, setPageAnchored] = useState<AnchoredText | null>(null);
|
|
86
93
|
const [displayDimensions, setDisplayDimensions] = useState<{ width: number; height: number } | null>(null);
|
|
87
94
|
const [scale] = useState(1.5); // Fixed scale for better quality
|
|
88
95
|
|
|
@@ -125,6 +132,36 @@ export function PdfAnnotationCanvas({
|
|
|
125
132
|
};
|
|
126
133
|
}, [pdfUrl]);
|
|
127
134
|
|
|
135
|
+
/**
|
|
136
|
+
* The server-derived map, fetched once per document rather than once per
|
|
137
|
+
* page-load effect (PERSIST-ANCHORS P4). The map is WHOLE-RESOURCE — one
|
|
138
|
+
* artifact covering every page — so re-reading it from the per-page effect
|
|
139
|
+
* meant a full refetch and re-decode on every page turn; on a 400-page scan
|
|
140
|
+
* that is one decode per interaction instead of one per document.
|
|
141
|
+
*
|
|
142
|
+
* The cache holds the in-flight promise so concurrent page loads share one
|
|
143
|
+
* fetch. Answers cache — including "no map" and a stored decline, which are
|
|
144
|
+
* definitive — but a transport failure clears the entry, so the next page
|
|
145
|
+
* load retries instead of pinning the whole document to geometry-only.
|
|
146
|
+
*/
|
|
147
|
+
const resourceAnchoredRef = useRef<{ uri: string; outcome: Promise<AnchoredText | null> } | null>(null);
|
|
148
|
+
const fetchResourceAnchored = useCallback((): Promise<AnchoredText | null> => {
|
|
149
|
+
if (!session) return Promise.resolve(null); // no session yet — don't cache its absence
|
|
150
|
+
const cached = resourceAnchoredRef.current;
|
|
151
|
+
if (cached && cached.uri === resourceUri) return cached.outcome;
|
|
152
|
+
|
|
153
|
+
const uri = resourceUri;
|
|
154
|
+
const outcome = session.client.browse.resourceAnchoredText(toResourceId(uri)).then(
|
|
155
|
+
(served) => (served && !('declined' in served) ? served : null),
|
|
156
|
+
() => {
|
|
157
|
+
if (resourceAnchoredRef.current?.uri === uri) resourceAnchoredRef.current = null;
|
|
158
|
+
return null;
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
resourceAnchoredRef.current = { uri, outcome };
|
|
162
|
+
return outcome;
|
|
163
|
+
}, [session, resourceUri]);
|
|
164
|
+
|
|
128
165
|
// Load current page when page number changes
|
|
129
166
|
useEffect(() => {
|
|
130
167
|
if (!pdfDoc) return;
|
|
@@ -132,6 +169,49 @@ export function PdfAnnotationCanvas({
|
|
|
132
169
|
let cancelled = false;
|
|
133
170
|
const doc = pdfDoc;
|
|
134
171
|
|
|
172
|
+
// Never quote the previous page's text under this page's rectangles.
|
|
173
|
+
setPageAnchored(null);
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The map a rectangle on this page quotes from, or null when there is
|
|
177
|
+
* none to be had.
|
|
178
|
+
*
|
|
179
|
+
* Never rejects, which is what lets the render run alongside it: quoting
|
|
180
|
+
* is the optional half of loading a page, so a failure here degrades to
|
|
181
|
+
* geometry-only rather than reaching the caller's error path. It is also
|
|
182
|
+
* the only reason `Promise.all` below is safe — a rejection from either
|
|
183
|
+
* side would leave the other promise dangling, and only the render can
|
|
184
|
+
* reject.
|
|
185
|
+
*/
|
|
186
|
+
async function resolveAnchored(page: Awaited<ReturnType<typeof doc.getPage>>): Promise<AnchoredText | null> {
|
|
187
|
+
try {
|
|
188
|
+
// The page's text layer, read once here rather than at drag time —
|
|
189
|
+
// `handleMouseUp` stays synchronous, and a native page costs nothing
|
|
190
|
+
// extra since pdf.js already parsed it to draw the page.
|
|
191
|
+
const runs = (await page.getTextContent()).items.filter(isTextRun);
|
|
192
|
+
if (runs.length > 0) return anchorRuns(runs, pageNumber);
|
|
193
|
+
|
|
194
|
+
// No runs means a scanned page: the characters exist only as pixels
|
|
195
|
+
// and pdf.js has nothing to give. The server derived a map at ingest,
|
|
196
|
+
// so ask for it rather than leaving the annotation anonymous.
|
|
197
|
+
// Whole-resource — served once per document via the cache above —
|
|
198
|
+
// and `textUnder` filters by page: the same shape the native branch
|
|
199
|
+
// produces, so nothing downstream branches.
|
|
200
|
+
//
|
|
201
|
+
// `null` is the ordinary answer for a document that has no map and
|
|
202
|
+
// never will; a failure is equally non-fatal. Either way the
|
|
203
|
+
// annotation ships with geometry only, which is what shipped before
|
|
204
|
+
// this existed. The served record is the full extraction outcome
|
|
205
|
+
// (PERSIST-ANCHORS D1); a stored decline means extraction ran and
|
|
206
|
+
// found nothing to anchor — for this canvas the same degradation as
|
|
207
|
+
// no map at all. A success outcome IS the anchoring shape, plus
|
|
208
|
+
// provenance this canvas does not read.
|
|
209
|
+
return await fetchResourceAnchored();
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
135
215
|
async function loadPage() {
|
|
136
216
|
try {
|
|
137
217
|
const page = await doc.getPage(pageNumber);
|
|
@@ -145,11 +225,21 @@ export function PdfAnnotationCanvas({
|
|
|
145
225
|
height: viewport.height
|
|
146
226
|
});
|
|
147
227
|
|
|
148
|
-
//
|
|
149
|
-
|
|
228
|
+
// Anchoring and rendering are independent, and only one of them the
|
|
229
|
+
// reader is waiting on. Sequencing them put a network round-trip in
|
|
230
|
+
// front of the pixels on exactly the documents that need it most: a
|
|
231
|
+
// scanned page fetches its map from the server, and rendering behind
|
|
232
|
+
// that await is how "failing to quote it must not fail to show it"
|
|
233
|
+
// became true of errors but not of latency. Started together, the
|
|
234
|
+
// page appears on its own schedule.
|
|
235
|
+
const [anchored, { dataUrl }] = await Promise.all([
|
|
236
|
+
resolveAnchored(page),
|
|
237
|
+
renderPdfPageToDataUrl(page, scale),
|
|
238
|
+
]);
|
|
150
239
|
|
|
151
240
|
if (cancelled) return;
|
|
152
241
|
|
|
242
|
+
setPageAnchored(anchored);
|
|
153
243
|
setPageImageUrl(dataUrl);
|
|
154
244
|
} catch (err) {
|
|
155
245
|
if (cancelled) return;
|
|
@@ -164,7 +254,7 @@ export function PdfAnnotationCanvas({
|
|
|
164
254
|
return () => {
|
|
165
255
|
cancelled = true;
|
|
166
256
|
};
|
|
167
|
-
}, [pdfDoc, pageNumber, scale]);
|
|
257
|
+
}, [pdfDoc, pageNumber, scale, fetchResourceAnchored]);
|
|
168
258
|
|
|
169
259
|
// Update display dimensions on resize
|
|
170
260
|
useEffect(() => {
|
|
@@ -318,15 +408,25 @@ export function PdfAnnotationCanvas({
|
|
|
318
408
|
// Create FragmentSelector
|
|
319
409
|
const fragmentSelector = createFragmentSelector(pdfCoord);
|
|
320
410
|
|
|
411
|
+
// What the box was drawn around. Without it the annotation is a rectangle
|
|
412
|
+
// with no memory of its own content: the panel entry is blank, search over
|
|
413
|
+
// annotation text misses it, and an export has nothing to print. Empty on a
|
|
414
|
+
// scanned page or over an image — emit no quote at all rather than an empty
|
|
415
|
+
// one, which would assert the box was drawn around nothing.
|
|
416
|
+
const quoted = pageAnchored ? textUnder(pageAnchored, pdfCoord) : '';
|
|
417
|
+
|
|
321
418
|
// Emit annotation:requested event with FragmentSelector
|
|
322
419
|
if (selectedMotivation) {
|
|
323
420
|
session.client.mark.request(
|
|
324
421
|
toResourceId(resourceUri),
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
422
|
+
[
|
|
423
|
+
{
|
|
424
|
+
type: 'FragmentSelector',
|
|
425
|
+
conformsTo: 'http://tools.ietf.org/rfc/rfc3778',
|
|
426
|
+
value: fragmentSelector,
|
|
427
|
+
},
|
|
428
|
+
...(quoted ? [{ type: 'TextQuoteSelector' as const, exact: quoted }] : []),
|
|
429
|
+
],
|
|
330
430
|
selectedMotivation,
|
|
331
431
|
);
|
|
332
432
|
}
|
|
@@ -336,7 +436,7 @@ export function PdfAnnotationCanvas({
|
|
|
336
436
|
setIsDrawing(false);
|
|
337
437
|
// Note: We keep selection so the preview remains visible
|
|
338
438
|
// It will be cleared when drawingMode changes or user starts new selection
|
|
339
|
-
}, [isDrawing, selection, pageNumber, pageDimensions, displayDimensions, selectedMotivation, existingAnnotations, session, resourceUri]);
|
|
439
|
+
}, [isDrawing, selection, pageNumber, pageDimensions, displayDimensions, selectedMotivation, existingAnnotations, session, resourceUri, pageAnchored]);
|
|
340
440
|
|
|
341
441
|
// Every FragmentSelector rect on the current page — one per line for a
|
|
342
442
|
// multi-line (multi-selector) annotation, exactly one for a manual annotation.
|