@semiont/react-ui 0.5.24 → 0.5.26
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-6ZSF6ZTJ.js +677 -0
- package/dist/PdfAnnotationCanvas.client-6ZSF6ZTJ.js.map +1 -0
- package/dist/PdfAnnotationCanvas.client-JCJMXWWJ.css +180 -0
- package/dist/PdfAnnotationCanvas.client-JCJMXWWJ.css.map +1 -0
- package/dist/{ar-JRTKCMTF.js → ar-YTUUGK2O.js} +32 -6
- package/dist/ar-YTUUGK2O.js.map +1 -0
- package/dist/{bn-LO6EDSCC.js → bn-ASGNGPMD.js} +32 -6
- package/dist/bn-ASGNGPMD.js.map +1 -0
- package/dist/{chunk-4QMTEPQ3.js → chunk-BTUG3VJJ.js} +18 -298
- package/dist/chunk-BTUG3VJJ.js.map +1 -0
- package/dist/{chunk-5Y5JDCKC.js → chunk-L3WKRWP3.js} +32 -6
- package/dist/chunk-L3WKRWP3.js.map +1 -0
- package/dist/chunk-OBQOZCJE.js +287 -0
- package/dist/chunk-OBQOZCJE.js.map +1 -0
- package/dist/{cs-LCHHOR4O.js → cs-WMYVAOZY.js} +32 -6
- package/dist/cs-WMYVAOZY.js.map +1 -0
- package/dist/{da-TARGPMJR.js → da-FSHHN4D6.js} +32 -6
- package/dist/da-FSHHN4D6.js.map +1 -0
- package/dist/{de-SBH4QBPJ.js → de-FJFO3TPT.js} +32 -6
- package/dist/de-FJFO3TPT.js.map +1 -0
- package/dist/{el-2D2OLGWB.js → el-XOH4QIAG.js} +32 -6
- package/dist/el-XOH4QIAG.js.map +1 -0
- package/dist/{en-QFIEG3CA.js → en-3FCBW3YD.js} +6 -2
- package/dist/{es-U6YK2LX6.js → es-HDX4QI7E.js} +32 -6
- package/dist/es-HDX4QI7E.js.map +1 -0
- package/dist/{fa-S7SZE37M.js → fa-DJ6WTFTJ.js} +32 -6
- package/dist/fa-DJ6WTFTJ.js.map +1 -0
- package/dist/{fi-CBL3NIXN.js → fi-4S7H7W7U.js} +32 -6
- package/dist/fi-4S7H7W7U.js.map +1 -0
- package/dist/{fr-EABUHV6S.js → fr-6UJSYHXC.js} +32 -6
- package/dist/fr-6UJSYHXC.js.map +1 -0
- package/dist/{he-THWD244Y.js → he-ZZJOHJX2.js} +32 -6
- package/dist/he-ZZJOHJX2.js.map +1 -0
- package/dist/{hi-GHGFZJ44.js → hi-HZJTAW7U.js} +32 -6
- package/dist/hi-HZJTAW7U.js.map +1 -0
- package/dist/{id-POZOUMDW.js → id-E3I6ILEM.js} +32 -6
- package/dist/id-E3I6ILEM.js.map +1 -0
- package/dist/index.css +140 -94
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +85 -24
- package/dist/index.js +1782 -1697
- package/dist/index.js.map +1 -1
- package/dist/{it-KDYJADSH.js → it-ULAPAD7U.js} +32 -6
- package/dist/it-ULAPAD7U.js.map +1 -0
- package/dist/{ja-Z5Z6ZF3P.js → ja-SAHZQ4CZ.js} +32 -6
- package/dist/ja-SAHZQ4CZ.js.map +1 -0
- package/dist/{ko-7X3XUXRN.js → ko-DFU2ADQ4.js} +32 -6
- package/dist/ko-DFU2ADQ4.js.map +1 -0
- package/dist/{ms-BTWEIW43.js → ms-NWL76XNT.js} +32 -6
- package/dist/ms-NWL76XNT.js.map +1 -0
- package/dist/{nl-IS2CGF5I.js → nl-ATCM76WN.js} +32 -6
- package/dist/nl-ATCM76WN.js.map +1 -0
- package/dist/{no-AAMEVA26.js → no-UIAQ5RJF.js} +32 -6
- package/dist/no-UIAQ5RJF.js.map +1 -0
- package/dist/{pl-PVXUQYFT.js → pl-NOECQDXD.js} +32 -6
- package/dist/pl-NOECQDXD.js.map +1 -0
- package/dist/{pt-3ITQ76TL.js → pt-SVWIIUPC.js} +32 -6
- package/dist/pt-SVWIIUPC.js.map +1 -0
- package/dist/{ro-FRB4IIVS.js → ro-NR2DRTZG.js} +32 -6
- package/dist/ro-NR2DRTZG.js.map +1 -0
- package/dist/{sv-DS3RO24M.js → sv-STVJ37JP.js} +32 -6
- package/dist/sv-STVJ37JP.js.map +1 -0
- package/dist/test-utils.js +5 -3
- package/dist/test-utils.js.map +1 -1
- package/dist/{th-AX66MWWB.js → th-UWQR4K54.js} +32 -6
- package/dist/th-UWQR4K54.js.map +1 -0
- package/dist/{tr-7KTB67TT.js → tr-WNXZSD27.js} +32 -6
- package/dist/tr-WNXZSD27.js.map +1 -0
- package/dist/{uk-SMQS2MAQ.js → uk-SJRMB47Y.js} +32 -6
- package/dist/uk-SJRMB47Y.js.map +1 -0
- package/dist/{vi-D3JXL2EZ.js → vi-TMRQFES2.js} +32 -6
- package/dist/vi-TMRQFES2.js.map +1 -0
- package/dist/{zh-S754AWFS.js → zh-QOL3LYEM.js} +32 -6
- package/dist/zh-QOL3LYEM.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/StatusDisplay.css +35 -35
- package/src/components/__tests__/AssistProgress.test.tsx +86 -28
- package/src/components/annotation/annotations.css +1 -1
- package/src/components/modals/SearchModal.css +5 -5
- package/src/components/pdf-annotation/PdfAnnotationCanvas.css +143 -6
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +689 -198
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +1008 -3
- package/src/components/pdf-annotation/__tests__/estimate-slot-height.test.ts +53 -0
- package/src/components/pdf-annotation/estimate-slot-height.ts +31 -0
- 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__/browse-renderers.dispatch.test.tsx +33 -0
- package/src/components/resource/__tests__/download-link.test.tsx +129 -0
- package/src/components/resource/annotate-renderers.tsx +10 -1
- package/src/components/resource/browse-renderers.tsx +10 -1
- 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/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__/ReferencesPanel.test.tsx +26 -0
- package/src/features/auth/auth.css +10 -10
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +58 -3
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +18 -7
- package/src/styles/core/inputs.css +1 -1
- package/src/styles/features/compose.css +1 -1
- package/src/styles/features/resource-viewer.css +1 -1
- package/src/styles/features/static-pages.css +11 -11
- package/src/styles/motivations/motivation-comment.css +2 -2
- package/src/styles/motivations/motivation-reference.css +2 -2
- package/src/styles/patterns/panels-base.css +4 -4
- package/src/styles/utilities/motion.css +15 -15
- package/src/styles/variables.css +29 -1
- package/translations/ar.json +27 -5
- package/translations/bn.json +27 -5
- package/translations/cs.json +27 -5
- package/translations/da.json +27 -5
- package/translations/de.json +27 -5
- package/translations/el.json +27 -5
- package/translations/en.json +27 -5
- package/translations/es.json +27 -5
- package/translations/fa.json +27 -5
- package/translations/fi.json +27 -5
- package/translations/fr.json +27 -5
- package/translations/he.json +27 -5
- package/translations/hi.json +27 -5
- package/translations/id.json +27 -5
- package/translations/it.json +27 -5
- package/translations/ja.json +27 -5
- package/translations/ko.json +27 -5
- package/translations/ms.json +27 -5
- package/translations/nl.json +27 -5
- package/translations/no.json +27 -5
- package/translations/pl.json +27 -5
- package/translations/pt.json +27 -5
- package/translations/ro.json +27 -5
- package/translations/sv.json +27 -5
- package/translations/th.json +27 -5
- package/translations/tr.json +27 -5
- package/translations/uk.json +27 -5
- package/translations/vi.json +27 -5
- package/translations/zh.json +27 -5
- package/dist/PdfAnnotationCanvas.client-SU5KNT7G.css +0 -98
- package/dist/PdfAnnotationCanvas.client-SU5KNT7G.css.map +0 -1
- package/dist/PdfAnnotationCanvas.client-V7Q3BEEQ.js +0 -427
- 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-4QMTEPQ3.js.map +0 -1
- package/dist/chunk-5Y5JDCKC.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-3FCBW3YD.js.map} +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF-CONTINUOUS-SCROLL S1b.
|
|
3
|
+
*
|
|
4
|
+
* The scroll column reserves space for unmounted pages. If the reservation is
|
|
5
|
+
* wrong, every mount and unmount changes the column's height and the scrollbar
|
|
6
|
+
* jumps — which is what S1 shipped, by reserving the raster height rather than
|
|
7
|
+
* the displayed one.
|
|
8
|
+
*
|
|
9
|
+
* These pins hold the estimate to the CSS the image is actually under:
|
|
10
|
+
* `max-width: 100%; height: auto`.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import { estimateSlotHeight } from '../estimate-slot-height';
|
|
14
|
+
|
|
15
|
+
// US Letter at scale 1.5: 918 × 1188 raster, aspect 792/612.
|
|
16
|
+
const ASPECT = 792 / 612;
|
|
17
|
+
const RASTER_W = 918;
|
|
18
|
+
|
|
19
|
+
describe('estimateSlotHeight', () => {
|
|
20
|
+
it('scales to the column when the column is narrower than the raster', () => {
|
|
21
|
+
// The common case: `max-width: 100%` shrinks the image to the column.
|
|
22
|
+
expect(estimateSlotHeight(600, RASTER_W, ASPECT)).toBe(Math.round(600 * ASPECT));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('never upscales past the raster — max-width shrinks only', () => {
|
|
26
|
+
// A column wider than the raster leaves the image at natural size, so the
|
|
27
|
+
// height stops growing. Reserving columnWidth * aspect here would over-
|
|
28
|
+
// reserve and leave a gap under every page.
|
|
29
|
+
expect(estimateSlotHeight(1600, RASTER_W, ASPECT)).toBe(Math.round(RASTER_W * ASPECT));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('is exactly the raster height when the column matches the raster', () => {
|
|
33
|
+
expect(estimateSlotHeight(RASTER_W, RASTER_W, ASPECT)).toBe(Math.round(RASTER_W * ASPECT));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('does NOT use the raster height as a width-independent constant', () => {
|
|
37
|
+
// The S1 defect, stated as a pin: the raster is 1188px tall, but in a
|
|
38
|
+
// 600px column the page occupies ~776px. Reserving 1188 is what made the
|
|
39
|
+
// column's height lurch on every mount.
|
|
40
|
+
const rasterHeight = Math.round(RASTER_W * ASPECT); // 1188
|
|
41
|
+
expect(estimateSlotHeight(600, RASTER_W, ASPECT)).not.toBe(rasterHeight);
|
|
42
|
+
expect(estimateSlotHeight(600, RASTER_W, ASPECT)).toBeLessThan(rasterHeight);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('answers null until every measurement is in, rather than guessing', () => {
|
|
46
|
+
// A wrong reservation is worse than none: an unsized slot is stable, a
|
|
47
|
+
// mis-sized one moves when the page lands.
|
|
48
|
+
expect(estimateSlotHeight(null, RASTER_W, ASPECT)).toBeNull();
|
|
49
|
+
expect(estimateSlotHeight(600, null, ASPECT)).toBeNull();
|
|
50
|
+
expect(estimateSlotHeight(600, RASTER_W, null)).toBeNull();
|
|
51
|
+
expect(estimateSlotHeight(0, RASTER_W, ASPECT)).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The height a page will occupy once rendered, computed BEFORE rendering it.
|
|
3
|
+
*
|
|
4
|
+
* The scrolling column reserves space for pages it has not mounted. If the
|
|
5
|
+
* reserved height differs from the height the page actually takes, every mount
|
|
6
|
+
* and unmount changes the column's total height and the scrollbar jumps under
|
|
7
|
+
* the reader's cursor — the defect S1 shipped by reserving the *raster* height
|
|
8
|
+
* (`getViewport({ scale }).height`) instead of the *displayed* one.
|
|
9
|
+
*
|
|
10
|
+
* This models the CSS the image is under exactly:
|
|
11
|
+
*
|
|
12
|
+
* .semiont-pdf-annotation-canvas__image { max-width: 100%; height: auto; }
|
|
13
|
+
*
|
|
14
|
+
* `max-width` shrinks but never upscales, so the displayed width is
|
|
15
|
+
* `min(columnWidth, rasterWidth)` and the height follows the page's aspect
|
|
16
|
+
* ratio. Aspect is scale-invariant, so it can come from any viewport.
|
|
17
|
+
*
|
|
18
|
+
* @param columnWidth Measured inner width of the scroll column, in CSS px.
|
|
19
|
+
* @param rasterWidth Width of the rendered raster, in px (`viewport(scale).width`).
|
|
20
|
+
* @param aspect Page height ÷ width, from any viewport scale.
|
|
21
|
+
* @returns Reserved height in CSS px, or null when a measurement is not in yet.
|
|
22
|
+
*/
|
|
23
|
+
export function estimateSlotHeight(
|
|
24
|
+
columnWidth: number | null,
|
|
25
|
+
rasterWidth: number | null,
|
|
26
|
+
aspect: number | null,
|
|
27
|
+
): number | null {
|
|
28
|
+
if (!columnWidth || !rasterWidth || !aspect) return null;
|
|
29
|
+
if (columnWidth <= 0 || rasterWidth <= 0 || aspect <= 0) return null;
|
|
30
|
+
return Math.round(Math.min(columnWidth, rasterWidth) * aspect);
|
|
31
|
+
}
|
|
@@ -5,9 +5,11 @@ import { capabilitiesOf, resourceId as toResourceId } from '@semiont/core';
|
|
|
5
5
|
import { ANNOTATORS } from '../../lib/annotation-registry';
|
|
6
6
|
import { buildTextSelectors, fallbackTextPosition } from '../../lib/text-selection-handler';
|
|
7
7
|
import { defaultAnnotateRenderers, type AnnotateMediaRenderers } from './annotate-renderers';
|
|
8
|
+
import { DownloadFileLink } from './DownloadFileLink';
|
|
8
9
|
import type { EditorView } from '@codemirror/view';
|
|
9
10
|
import type { SemiontSession } from '@semiont/sdk';
|
|
10
11
|
import { useSessionEventSubscriptions } from '../../hooks/useSessionEventSubscriptions';
|
|
12
|
+
import { scrollAnnotationIntoView } from '../../lib/scroll-utils';
|
|
11
13
|
|
|
12
14
|
// Type augmentation for custom DOM properties
|
|
13
15
|
interface EnrichedHTMLElement extends HTMLElement {
|
|
@@ -51,6 +53,7 @@ interface Props {
|
|
|
51
53
|
*
|
|
52
54
|
* @emits mark:requested - User requested to create annotation. Payload: { selector: Selector | Selector[], motivation: SelectionMotivation }
|
|
53
55
|
* @subscribes beckon:hover - Annotation hovered. Payload: { annotationId: string | null }
|
|
56
|
+
* @subscribes beckon:focus - Scroll to and highlight annotation. Payload: { annotationId: string }
|
|
54
57
|
*/
|
|
55
58
|
export function AnnotateView({
|
|
56
59
|
content,
|
|
@@ -104,10 +107,20 @@ export function AnnotateView({
|
|
|
104
107
|
onUIStateChangeRef.current?.({ hoveredAnnotationId: annotationId });
|
|
105
108
|
}, []);
|
|
106
109
|
|
|
110
|
+
// "Scroll to and highlight this annotation" — the same contract BrowseView
|
|
111
|
+
// serves, so the behaviour no longer depends on which view mode is active.
|
|
112
|
+
// The `scrollToAnnotationId` prop path (uiState → renderer) stays as the
|
|
113
|
+
// host-facing capability it is; this is the in-app producer's route.
|
|
114
|
+
const handleAnnotationFocus = useCallback(({ annotationId }: { annotationId?: string | null }) => {
|
|
115
|
+
if (!containerRef.current) return;
|
|
116
|
+
scrollAnnotationIntoView(annotationId ?? null, containerRef.current, { pulse: true });
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
107
119
|
// Annotation hover (session-scoped). Toolbar preference changes flow through
|
|
108
120
|
// props/callbacks, not the bus (TOOLBAR-PREFS-AS-PROPS).
|
|
109
121
|
useSessionEventSubscriptions(session, {
|
|
110
122
|
'beckon:hover': handleAnnotationHover,
|
|
123
|
+
'beckon:focus': handleAnnotationFocus,
|
|
111
124
|
});
|
|
112
125
|
|
|
113
126
|
// Handle text annotation with sparkle or immediate creation
|
|
@@ -210,13 +223,7 @@ export function AnnotateView({
|
|
|
210
223
|
<p className="semiont-annotate-view__empty-message">
|
|
211
224
|
Annotation not supported for {mimeType}
|
|
212
225
|
</p>
|
|
213
|
-
<
|
|
214
|
-
href={`/api/resources/${resourceUri}`}
|
|
215
|
-
download
|
|
216
|
-
className="semiont-button semiont-button--primary"
|
|
217
|
-
>
|
|
218
|
-
Download File
|
|
219
|
-
</a>
|
|
226
|
+
<DownloadFileLink resourceUri={resourceUri} session={session} />
|
|
220
227
|
</div>
|
|
221
228
|
</div>
|
|
222
229
|
);
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import type { SemiontSession } from '@semiont/sdk';
|
|
22
22
|
import { useSessionEventSubscriptions } from '../../hooks/useSessionEventSubscriptions';
|
|
23
23
|
import { defaultBrowseRenderers, type BrowseMediaRenderers } from './browse-renderers';
|
|
24
|
+
import { DownloadFileLink } from './DownloadFileLink';
|
|
24
25
|
|
|
25
26
|
interface Props {
|
|
26
27
|
content: string;
|
|
@@ -295,13 +296,7 @@ export const BrowseView = memo(function BrowseView({
|
|
|
295
296
|
<p className="semiont-browse-view__empty-message">
|
|
296
297
|
Preview not available for {mimeType}
|
|
297
298
|
</p>
|
|
298
|
-
<
|
|
299
|
-
href={`/api/resources/${resourceUri}`}
|
|
300
|
-
download
|
|
301
|
-
className="semiont-button semiont-button--primary"
|
|
302
|
-
>
|
|
303
|
-
Download File
|
|
304
|
-
</a>
|
|
299
|
+
<DownloadFileLink resourceUri={resourceUri} session={session} />
|
|
305
300
|
</div>
|
|
306
301
|
</div>
|
|
307
302
|
);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { resourceId as toResourceId } from '@semiont/core';
|
|
4
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
5
|
+
import { useMediaToken } from '../../hooks/useMediaToken';
|
|
6
|
+
import { mediaUrl } from '../../lib/media-url';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/** The '@id' of the resource whose bytes to offer. */
|
|
10
|
+
resourceUri: string;
|
|
11
|
+
/** Session for that resource — its client mints the media token and supplies the backend origin. */
|
|
12
|
+
session: SemiontSession | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The "Download File" affordance on the unsupported-media fallback, shared by
|
|
17
|
+
* the browse and annotate views.
|
|
18
|
+
*
|
|
19
|
+
* Its own component rather than inline markup for two reasons. The token is a
|
|
20
|
+
* hook, and this fallback is a branch — mounting the component only in that
|
|
21
|
+
* branch keeps every text/image/PDF view from minting a media token it will
|
|
22
|
+
* never use. And the URL rule (absolute + `?token=`) lives in `mediaUrl`,
|
|
23
|
+
* where both views and the viewer page read the same one.
|
|
24
|
+
*
|
|
25
|
+
* Before the token lands the anchor renders as a disabled button: a tokenless
|
|
26
|
+
* URL is a guaranteed 401, and a link that silently fails is worse than one
|
|
27
|
+
* visibly not ready yet. A client with no `auth` namespace (a host on a bare
|
|
28
|
+
* transport) stays in that state permanently, which is the honest answer —
|
|
29
|
+
* it cannot mint tokens at all.
|
|
30
|
+
*/
|
|
31
|
+
export function DownloadFileLink({ resourceUri, session }: Props) {
|
|
32
|
+
const client = session?.client ?? null;
|
|
33
|
+
const { token } = useMediaToken(client, toResourceId(resourceUri));
|
|
34
|
+
const href = mediaUrl(client, toResourceId(resourceUri), token);
|
|
35
|
+
|
|
36
|
+
if (!href) {
|
|
37
|
+
return (
|
|
38
|
+
<button type="button" disabled className="semiont-button semiont-button--primary semiont-button--disabled">
|
|
39
|
+
Download File
|
|
40
|
+
</button>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<a href={href} download className="semiont-button semiont-button--primary">
|
|
46
|
+
Download File
|
|
47
|
+
</a>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASSIST-SURFACE-WARTS Lane D — in-content scroll on `beckon:focus`.
|
|
3
|
+
*
|
|
4
|
+
* `beckon:focus` is the established "scroll to and highlight this annotation"
|
|
5
|
+
* contract; BrowseView has subscribed to it all along. AnnotateView did not,
|
|
6
|
+
* so the same event scrolled the content in browse mode and did nothing in
|
|
7
|
+
* annotate mode. With the history panel now producing the event, that asymmetry
|
|
8
|
+
* becomes user-visible: the same click works or doesn't depending on the mode.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
11
|
+
import { render } from '@testing-library/react';
|
|
12
|
+
import '@testing-library/jest-dom';
|
|
13
|
+
import type { AnnotationUIState } from '../../../types/annotation-props';
|
|
14
|
+
import { createTestSemiontWrapper } from '../../../test-utils';
|
|
15
|
+
|
|
16
|
+
const scrollSpy = vi.hoisted(() => vi.fn());
|
|
17
|
+
vi.mock('../../../lib/scroll-utils', () => ({
|
|
18
|
+
scrollAnnotationIntoView: scrollSpy,
|
|
19
|
+
}));
|
|
20
|
+
vi.mock('../../CodeMirrorRenderer', () => ({
|
|
21
|
+
CodeMirrorRenderer: () => <div data-annotation-id="ann-7">cm-mock</div>,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import { AnnotateView } from '../AnnotateView';
|
|
25
|
+
|
|
26
|
+
const emptyAnnotations = { highlights: [], references: [], assessments: [], comments: [], tags: [] };
|
|
27
|
+
const uiState: AnnotationUIState = {
|
|
28
|
+
selectedMotivation: 'linking',
|
|
29
|
+
selectedClick: 'detail',
|
|
30
|
+
selectedShape: 'rectangle',
|
|
31
|
+
hoveredAnnotationId: null,
|
|
32
|
+
scrollToAnnotationId: null,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe('AnnotateView — beckon:focus scrolls the content', () => {
|
|
36
|
+
beforeEach(() => { scrollSpy.mockClear(); });
|
|
37
|
+
|
|
38
|
+
it('scrolls to the annotation when the session emits beckon:focus', () => {
|
|
39
|
+
const { session, client } = createTestSemiontWrapper();
|
|
40
|
+
|
|
41
|
+
render(
|
|
42
|
+
<AnnotateView
|
|
43
|
+
content="hello world"
|
|
44
|
+
mimeType="text/plain"
|
|
45
|
+
resourceUri="res-1"
|
|
46
|
+
annotations={emptyAnnotations}
|
|
47
|
+
uiState={uiState}
|
|
48
|
+
session={session}
|
|
49
|
+
annotateMode
|
|
50
|
+
/>,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
client.bus.get('beckon:focus').next({ annotationId: 'ann-7' });
|
|
54
|
+
|
|
55
|
+
expect(scrollSpy).toHaveBeenCalledTimes(1);
|
|
56
|
+
expect(scrollSpy.mock.calls[0]?.[0]).toBe('ann-7');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -109,5 +109,38 @@ describe('browse-renderers — annotation + session forwarding (dispatch contrac
|
|
|
109
109
|
expect(props.existingAnnotations).toEqual([shapeAnnotation()]); // pinned — worked before
|
|
110
110
|
expect(props.drawingMode).toBeNull();
|
|
111
111
|
expect(props.session).toBe(session); // NEW — was a session-less no-op
|
|
112
|
+
// PDF-CONTINUOUS-SCROLL S1: browse asks for the scrolling column.
|
|
113
|
+
expect(props.pageLayout).toBe('scroll');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// S2. Annotate is the mode people actually work in
|
|
117
|
+
// (.plans/PDF-CONTINUOUS-SCROLL.md D3, second correction), so it gets the
|
|
118
|
+
// column too — leaving it on Previous/Next made the feature invisible to
|
|
119
|
+
// its primary audience.
|
|
120
|
+
it('the annotate registry asks for the column as well, with the live tool', async () => {
|
|
121
|
+
const session = fakeSession();
|
|
122
|
+
const { AnnotateView } = await import('../AnnotateView');
|
|
123
|
+
const { container } = render(
|
|
124
|
+
<AnnotateView
|
|
125
|
+
content="blob:pdf-url"
|
|
126
|
+
mimeType="application/pdf"
|
|
127
|
+
resourceUri="res-1"
|
|
128
|
+
annotations={emptyAnnotations}
|
|
129
|
+
uiState={{
|
|
130
|
+
selectedMotivation: 'highlighting',
|
|
131
|
+
selectedClick: 'detail',
|
|
132
|
+
selectedShape: 'rectangle',
|
|
133
|
+
hoveredAnnotationId: null,
|
|
134
|
+
scrollToAnnotationId: null,
|
|
135
|
+
}}
|
|
136
|
+
annotateMode
|
|
137
|
+
session={session}
|
|
138
|
+
/>,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
await within(container).findByText('pdf-canvas-mock');
|
|
142
|
+
const props = captured.pdf.at(-1)!;
|
|
143
|
+
expect(props.pageLayout).toBe('scroll');
|
|
144
|
+
expect(props.drawingMode).toBe('rectangle'); // the tool still reaches the pages
|
|
112
145
|
});
|
|
113
146
|
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "Download File" affordance on the unsupported-media fallback, in BOTH
|
|
3
|
+
* resource views.
|
|
4
|
+
*
|
|
5
|
+
* `GET /api/resources/:id` is the browser-facing alias of the pipe, and it
|
|
6
|
+
* exists only as an auth affordance for `<img>`, PDF.js and download links: a
|
|
7
|
+
* plain `<a download href>` sends neither an Authorization header nor a cookie,
|
|
8
|
+
* so the alias takes bearer + `?token=` only. Both views shipped a bare
|
|
9
|
+
* `/api/resources/${id}` — tokenless, so a 401, AND relative, so under
|
|
10
|
+
* bring-your-own-session embedding it resolves against the HOST app's origin
|
|
11
|
+
* rather than the backend's.
|
|
12
|
+
*
|
|
13
|
+
* Started RED (both hrefs were `/api/resources/res-1`).
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
16
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
17
|
+
import '@testing-library/jest-dom';
|
|
18
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
19
|
+
import type { AnnotationUIState } from '../../../types/annotation-props';
|
|
20
|
+
import { AnnotateView } from '../AnnotateView';
|
|
21
|
+
import { BrowseView } from '../BrowseView';
|
|
22
|
+
|
|
23
|
+
vi.mock('../../annotation/AnnotateToolbar', () => ({ AnnotateToolbar: () => null }));
|
|
24
|
+
vi.mock('../../CodeMirrorRenderer', () => ({ CodeMirrorRenderer: () => null }));
|
|
25
|
+
vi.mock('../../image-annotation/SvgDrawingCanvas', () => ({ SvgDrawingCanvas: () => null }));
|
|
26
|
+
vi.mock('react-markdown', () => ({ default: ({ children }: { children: string }) => <div>{children}</div> }));
|
|
27
|
+
vi.mock('remark-gfm', () => ({ default: () => ({}) }));
|
|
28
|
+
|
|
29
|
+
const BACKEND = 'http://backend.test:4000';
|
|
30
|
+
const UNSUPPORTED = 'application/octet-stream';
|
|
31
|
+
|
|
32
|
+
const emptyAnnotations = { highlights: [], references: [], assessments: [], comments: [], tags: [] };
|
|
33
|
+
const uiState: AnnotationUIState = {
|
|
34
|
+
selectedMotivation: null,
|
|
35
|
+
selectedClick: 'detail',
|
|
36
|
+
selectedShape: 'rectangle',
|
|
37
|
+
hoveredAnnotationId: null,
|
|
38
|
+
scrollToAnnotationId: null,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** A session whose client mints `token` — the batteries-included case. */
|
|
42
|
+
function sessionMinting(token: string): SemiontSession {
|
|
43
|
+
return fakeSession({ auth: { mediaToken: vi.fn(async () => ({ token })) } });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A session whose token request never settles — the in-flight case. */
|
|
47
|
+
function sessionPending(): SemiontSession {
|
|
48
|
+
return fakeSession({ auth: { mediaToken: vi.fn(() => new Promise<never>(() => {})) } });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A transport-only client: `SemiontClient.auth` is `AuthNamespace | undefined`,
|
|
53
|
+
* and a host wiring a bare transport (no `IBackendOperations`) has no `auth`.
|
|
54
|
+
*/
|
|
55
|
+
function sessionWithoutAuth(): SemiontSession {
|
|
56
|
+
return fakeSession({});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function fakeSession(clientExtras: Record<string, unknown>): SemiontSession {
|
|
60
|
+
return {
|
|
61
|
+
client: {
|
|
62
|
+
baseUrl: BACKEND,
|
|
63
|
+
mark: { request: vi.fn() },
|
|
64
|
+
browse: { click: vi.fn() },
|
|
65
|
+
beckon: { hover: vi.fn() },
|
|
66
|
+
...clientExtras,
|
|
67
|
+
},
|
|
68
|
+
subscribe: () => () => {},
|
|
69
|
+
} as unknown as SemiontSession;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const annotateProps = (session: SemiontSession) => ({
|
|
73
|
+
content: '',
|
|
74
|
+
mimeType: UNSUPPORTED,
|
|
75
|
+
resourceUri: 'res-1',
|
|
76
|
+
annotations: emptyAnnotations,
|
|
77
|
+
uiState,
|
|
78
|
+
annotateMode: false,
|
|
79
|
+
session,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const browseProps = (session: SemiontSession) => ({
|
|
83
|
+
content: '',
|
|
84
|
+
mimeType: UNSUPPORTED,
|
|
85
|
+
resourceUri: 'res-1',
|
|
86
|
+
annotations: emptyAnnotations,
|
|
87
|
+
annotateMode: false,
|
|
88
|
+
session,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const VIEWS = [
|
|
92
|
+
{ name: 'AnnotateView', render: (s: SemiontSession) => render(<AnnotateView {...annotateProps(s)} />) },
|
|
93
|
+
{ name: 'BrowseView', render: (s: SemiontSession) => render(<BrowseView {...browseProps(s)} />) },
|
|
94
|
+
] as const;
|
|
95
|
+
|
|
96
|
+
describe.each(VIEWS)('$name — unsupported-media download link', ({ render: renderView }) => {
|
|
97
|
+
it('points at the backend origin and carries the media token', async () => {
|
|
98
|
+
renderView(sessionMinting('tok-abc'));
|
|
99
|
+
|
|
100
|
+
const link = await screen.findByRole('link', { name: 'Download File' });
|
|
101
|
+
expect(link).toHaveAttribute('href', `${BACKEND}/api/resources/res-1?token=tok-abc`);
|
|
102
|
+
expect(link).toHaveAttribute('download');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('renders no href while the token is still in flight', async () => {
|
|
106
|
+
renderView(sessionPending());
|
|
107
|
+
|
|
108
|
+
// The affordance stays on screen — it is just not yet actionable. What it
|
|
109
|
+
// must NOT do is offer a URL already known to fail.
|
|
110
|
+
expect(await screen.findByText('Download File')).toBeInTheDocument();
|
|
111
|
+
expect(screen.queryByRole('link', { name: 'Download File' })).not.toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('renders no href when the client has no auth namespace', async () => {
|
|
115
|
+
renderView(sessionWithoutAuth());
|
|
116
|
+
|
|
117
|
+
expect(await screen.findByText('Download File')).toBeInTheDocument();
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
expect(screen.queryByRole('link', { name: 'Download File' })).not.toBeInTheDocument();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('never builds a host-relative URL', async () => {
|
|
124
|
+
renderView(sessionMinting('tok-abc'));
|
|
125
|
+
|
|
126
|
+
const link = await screen.findByRole('link', { name: 'Download File' });
|
|
127
|
+
expect(link.getAttribute('href')).not.toMatch(/^\//);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -7,10 +7,18 @@ import { SvgDrawingCanvas } from '../image-annotation/SvgDrawingCanvas';
|
|
|
7
7
|
import { CodeMirrorRenderer } from '../CodeMirrorRenderer';
|
|
8
8
|
import { segmentTextWithAnnotations } from '../../lib/text-segmentation';
|
|
9
9
|
import type { SelectionMotivation, ShapeType } from '../annotation/AnnotateToolbar';
|
|
10
|
+
import { useTranslations } from '../../contexts/TranslationContext';
|
|
10
11
|
|
|
11
12
|
// Lazy-load the PDF component to avoid SSR issues with browser PDF.js loading.
|
|
12
13
|
// Kept lazy here deliberately: hoisting it to a static import would make it
|
|
13
14
|
// eager again and reintroduce the SSR problem this indirection exists for.
|
|
15
|
+
/** The lazy chunk's fallback needs its own component: a hook cannot run
|
|
16
|
+
* inside a `fallback` prop expression. */
|
|
17
|
+
function PdfViewerLoading() {
|
|
18
|
+
const t = useTranslations('PdfViewer');
|
|
19
|
+
return <>{t('viewerLoading')}</>;
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
const PdfAnnotationCanvas = lazy(() =>
|
|
15
23
|
import('../pdf-annotation/PdfAnnotationCanvas.client').then((mod) => ({ default: mod.PdfAnnotationCanvas })),
|
|
16
24
|
);
|
|
@@ -127,7 +135,7 @@ export function PdfAnnotateRenderer({
|
|
|
127
135
|
}: AnnotateMediaRendererProps) {
|
|
128
136
|
if (!content) return null;
|
|
129
137
|
return (
|
|
130
|
-
<Suspense fallback={<div className="semiont-annotate-view__loading"
|
|
138
|
+
<Suspense fallback={<div className="semiont-annotate-view__loading"><PdfViewerLoading /></div>}>
|
|
131
139
|
<PdfAnnotationCanvas
|
|
132
140
|
pdfUrl={content}
|
|
133
141
|
resourceUri={resourceUri}
|
|
@@ -137,6 +145,7 @@ export function PdfAnnotateRenderer({
|
|
|
137
145
|
session={session}
|
|
138
146
|
hoveredAnnotationId={hoveredAnnotationId || null}
|
|
139
147
|
hoverDelayMs={hoverDelayMs}
|
|
148
|
+
pageLayout="scroll"
|
|
140
149
|
/>
|
|
141
150
|
</Suspense>
|
|
142
151
|
);
|
|
@@ -6,8 +6,16 @@ import remarkGfm from 'remark-gfm';
|
|
|
6
6
|
import type { Annotation } from '@semiont/core';
|
|
7
7
|
import type { SemiontSession } from '@semiont/sdk';
|
|
8
8
|
import { SvgDrawingCanvas } from '../image-annotation/SvgDrawingCanvas';
|
|
9
|
+
import { useTranslations } from '../../contexts/TranslationContext';
|
|
9
10
|
|
|
10
11
|
// Lazy-load the PDF component to avoid SSR issues with browser PDF.js loading.
|
|
12
|
+
/** The lazy chunk's fallback needs its own component: a hook cannot run
|
|
13
|
+
* inside a `fallback` prop expression. */
|
|
14
|
+
function PdfViewerLoading() {
|
|
15
|
+
const t = useTranslations('PdfViewer');
|
|
16
|
+
return <>{t('viewerLoading')}</>;
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
const PdfAnnotationCanvas = lazy(() => import('../pdf-annotation/PdfAnnotationCanvas.client').then(mod => ({ default: mod.PdfAnnotationCanvas })));
|
|
12
20
|
|
|
13
21
|
/**
|
|
@@ -60,7 +68,7 @@ export function ImageBrowseRenderer({ content, resourceUri, annotations, session
|
|
|
60
68
|
|
|
61
69
|
export function PdfBrowseRenderer({ content, resourceUri, annotations, session }: MediaRendererProps) {
|
|
62
70
|
return (
|
|
63
|
-
<Suspense fallback={<div className="semiont-browse-view__loading"
|
|
71
|
+
<Suspense fallback={<div className="semiont-browse-view__loading"><PdfViewerLoading /></div>}>
|
|
64
72
|
<PdfAnnotationCanvas
|
|
65
73
|
pdfUrl={content}
|
|
66
74
|
resourceUri={resourceUri}
|
|
@@ -68,6 +76,7 @@ export function PdfBrowseRenderer({ content, resourceUri, annotations, session }
|
|
|
68
76
|
drawingMode={null}
|
|
69
77
|
selectedMotivation={null}
|
|
70
78
|
session={session}
|
|
79
|
+
pageLayout="scroll"
|
|
71
80
|
/>
|
|
72
81
|
</Suspense>
|
|
73
82
|
);
|
|
@@ -126,49 +126,6 @@
|
|
|
126
126
|
Detection Log (Completed Results)
|
|
127
127
|
============================================ */
|
|
128
128
|
|
|
129
|
-
.semiont-assist-widget__log {
|
|
130
|
-
background-color: var(--semiont-bg-primary);
|
|
131
|
-
border-radius: 0.5rem;
|
|
132
|
-
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
|
133
|
-
padding: 1rem;
|
|
134
|
-
display: flex;
|
|
135
|
-
flex-direction: column;
|
|
136
|
-
gap: 0.75rem;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
[data-theme="dark"] .semiont-assist-widget__log {
|
|
140
|
-
background-color: var(--semiont-color-gray-800);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
.semiont-assist-widget__log-items {
|
|
144
|
-
display: flex;
|
|
145
|
-
flex-direction: column;
|
|
146
|
-
gap: 0.25rem;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
.semiont-assist-widget__log-item {
|
|
150
|
-
font-size: 0.75rem;
|
|
151
|
-
color: var(--semiont-color-gray-600);
|
|
152
|
-
display: flex;
|
|
153
|
-
align-items: center;
|
|
154
|
-
gap: 0.5rem;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
[data-theme="dark"] .semiont-assist-widget__log-item {
|
|
158
|
-
color: var(--semiont-color-gray-400);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.semiont-assist-widget__log-check {
|
|
162
|
-
color: var(--semiont-color-green-600);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
[data-theme="dark"] .semiont-assist-widget__log-check {
|
|
166
|
-
color: var(--semiont-color-green-400);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
.semiont-assist-widget__log-type {
|
|
170
|
-
font-weight: 500;
|
|
171
|
-
}
|
|
172
129
|
|
|
173
130
|
/* ============================================
|
|
174
131
|
Detection Progress Display
|
|
@@ -43,6 +43,7 @@ export function AssistSection({
|
|
|
43
43
|
annotationType === 'assessment' ? 'AssessmentPanel' :
|
|
44
44
|
'CommentsPanel';
|
|
45
45
|
const t = useTranslations(panelName);
|
|
46
|
+
const ta = useTranslations('AssistProgress');
|
|
46
47
|
const [instructions, setInstructions] = useState('');
|
|
47
48
|
type ToneValue = 'scholarly' | 'explanatory' | 'conversational' | 'technical' | 'analytical' | 'critical' | 'balanced' | 'constructive' | '';
|
|
48
49
|
const [tone, setTone] = useState<ToneValue>('');
|
|
@@ -89,7 +90,15 @@ export function AssistSection({
|
|
|
89
90
|
progress={progress}
|
|
90
91
|
progressProps={{
|
|
91
92
|
onDismiss: handleDismissProgress,
|
|
92
|
-
translations: {
|
|
93
|
+
translations: {
|
|
94
|
+
cancel: t('cancel'),
|
|
95
|
+
inProgress: t('annotating'),
|
|
96
|
+
complete: t('complete'),
|
|
97
|
+
failed: t('failed'),
|
|
98
|
+
paramsTitle: ta('paramsTitle'),
|
|
99
|
+
processing: (label) => ta('processing', { label }),
|
|
100
|
+
close: ta('close'),
|
|
101
|
+
},
|
|
93
102
|
}}
|
|
94
103
|
form={
|
|
95
104
|
<>
|
|
@@ -157,7 +166,7 @@ export function AssistSection({
|
|
|
157
166
|
<span>{t('densityLabel')}</span>
|
|
158
167
|
</label>
|
|
159
168
|
{useDensity && (
|
|
160
|
-
<span className="semiont-form-field__info">{
|
|
169
|
+
<span className="semiont-form-field__info">{t('densityPerWords', { density })}</span>
|
|
161
170
|
)}
|
|
162
171
|
</div>
|
|
163
172
|
|
|
@@ -19,8 +19,12 @@ export interface AssistShellProps {
|
|
|
19
19
|
* Pass-through config for the progress renderer (cancel/dismiss wiring,
|
|
20
20
|
* translations, percent bar). Dismiss policy lives HERE: the shell forwards
|
|
21
21
|
* `onDismiss` only once the assist is no longer running.
|
|
22
|
+
*
|
|
23
|
+
* Required because `translations` is: the shell renders the progress display
|
|
24
|
+
* itself, so a caller that omitted this would render untranslated chrome —
|
|
25
|
+
* the failure Lane A exists to make impossible.
|
|
22
26
|
*/
|
|
23
|
-
progressProps
|
|
27
|
+
progressProps: Omit<AssistProgressProps, 'progress' | 'dataType'>;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/**
|
|
@@ -6,6 +6,7 @@ import type { SemiontSession } from '@semiont/sdk';
|
|
|
6
6
|
import { useSessionEventSubscriptions } from '../../../hooks/useSessionEventSubscriptions';
|
|
7
7
|
import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
|
|
8
8
|
import { AssistShell } from './AssistShell';
|
|
9
|
+
import { EntityFoundLog } from '../../EntityFoundLog';
|
|
9
10
|
import { ReferenceEntry } from './ReferenceEntry';
|
|
10
11
|
import type { components, Selector } from '@semiont/core';
|
|
11
12
|
import { getTextPositionSelector, getTargetSelector } from '@semiont/core';
|
|
@@ -121,6 +122,9 @@ export function ReferencesPanel({
|
|
|
121
122
|
sourceLanguage,
|
|
122
123
|
}: Props) {
|
|
123
124
|
const t = useTranslations('ReferencesPanel');
|
|
125
|
+
// Chrome shared by every assist surface (heading, current-work line, dismiss)
|
|
126
|
+
// lives in one namespace so the same words aren't re-translated per panel.
|
|
127
|
+
const ta = useTranslations('AssistProgress');
|
|
124
128
|
const [selectedEntityTypes, setSelectedEntityTypes] = useState<string[]>([]);
|
|
125
129
|
const [lastAnnotationLog, setLastDetectionLog] = useState<Array<{ entityType: string; foundCount: number }> | null>(null);
|
|
126
130
|
const [pendingEntityTypes, setPendingEntityTypes] = useState<string[]>([]);
|
|
@@ -375,24 +379,19 @@ export function ReferencesPanel({
|
|
|
375
379
|
failed: t('failed'),
|
|
376
380
|
found: (count) => t('found', { count }),
|
|
377
381
|
current: (entityType) => t('current', { entityType }),
|
|
378
|
-
|
|
382
|
+
paramsTitle: ta('paramsTitle'),
|
|
383
|
+
processing: (label) => ta('processing', { label }),
|
|
384
|
+
close: ta('close'),
|
|
379
385
|
},
|
|
380
386
|
}}
|
|
381
387
|
form={
|
|
382
388
|
<>
|
|
383
389
|
{/* Completed annotation log - shown after completion */}
|
|
384
|
-
{lastAnnotationLog &&
|
|
385
|
-
<
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
<span className="semiont-assist-widget__log-check">✓</span>
|
|
390
|
-
<span className="semiont-assist-widget__log-type">{item.entityType}:</span>
|
|
391
|
-
<span>{t('found', { count: item.foundCount })}</span>
|
|
392
|
-
</div>
|
|
393
|
-
))}
|
|
394
|
-
</div>
|
|
395
|
-
</div>
|
|
390
|
+
{lastAnnotationLog && (
|
|
391
|
+
<EntityFoundLog
|
|
392
|
+
entries={lastAnnotationLog}
|
|
393
|
+
formatFound={(count) => t('found', { count })}
|
|
394
|
+
/>
|
|
396
395
|
)}
|
|
397
396
|
|
|
398
397
|
{/* Entity Types Selection */}
|