@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
|
@@ -13,9 +13,35 @@ import userEvent from '@testing-library/user-event';
|
|
|
13
13
|
import { PdfAnnotationCanvas } from '../PdfAnnotationCanvas';
|
|
14
14
|
import { resourceId, annotationId, parseFragmentSelector } from '@semiont/core';
|
|
15
15
|
import { pdfToCanvasCoordinates } from '../../../lib/pdf-coordinates';
|
|
16
|
+
import { loadPdfDocument, renderPdfPageToDataUrl } from '../../../lib/browser-pdfjs';
|
|
16
17
|
|
|
17
18
|
import type { Annotation } from '@semiont/core';
|
|
18
19
|
|
|
20
|
+
// The page's text layer, as pdf.js reports it. Geometry is PDF points,
|
|
21
|
+
// bottom-left origin: "Hello world" sits on one line at y=700, "elsewhere" a
|
|
22
|
+
// hundred points below it.
|
|
23
|
+
// vi.hoisted: vi.mock's factory is lifted above these declarations.
|
|
24
|
+
const { MOCK_TEXT_ITEMS } = vi.hoisted(() => ({
|
|
25
|
+
MOCK_TEXT_ITEMS: [
|
|
26
|
+
{ str: 'Hello', transform: [1, 0, 0, 1, 72, 700], width: 30, height: 12, hasEOL: false },
|
|
27
|
+
{ str: 'world', transform: [1, 0, 0, 1, 106, 700], width: 32, height: 12, hasEOL: true },
|
|
28
|
+
{ str: 'elsewhere', transform: [1, 0, 0, 1, 72, 600], width: 54, height: 12, hasEOL: true },
|
|
29
|
+
],
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const mockPage = (textItems: unknown[]) => ({
|
|
33
|
+
getViewport: vi.fn().mockReturnValue({
|
|
34
|
+
width: 612,
|
|
35
|
+
height: 792,
|
|
36
|
+
scale: 1.0,
|
|
37
|
+
rotation: 0
|
|
38
|
+
}),
|
|
39
|
+
render: vi.fn().mockReturnValue({
|
|
40
|
+
promise: Promise.resolve()
|
|
41
|
+
}),
|
|
42
|
+
getTextContent: vi.fn().mockResolvedValue({ items: textItems })
|
|
43
|
+
});
|
|
44
|
+
|
|
19
45
|
// Mock browser-pdfjs module
|
|
20
46
|
vi.mock('../../../lib/browser-pdfjs', () => ({
|
|
21
47
|
loadPdfDocument: vi.fn().mockResolvedValue({
|
|
@@ -29,7 +55,8 @@ vi.mock('../../../lib/browser-pdfjs', () => ({
|
|
|
29
55
|
}),
|
|
30
56
|
render: vi.fn().mockReturnValue({
|
|
31
57
|
promise: Promise.resolve()
|
|
32
|
-
})
|
|
58
|
+
}),
|
|
59
|
+
getTextContent: vi.fn().mockResolvedValue({ items: MOCK_TEXT_ITEMS })
|
|
33
60
|
})
|
|
34
61
|
}),
|
|
35
62
|
renderPdfPageToDataUrl: vi.fn().mockResolvedValue({
|
|
@@ -313,6 +340,225 @@ describe('PdfAnnotationCanvas', () => {
|
|
|
313
340
|
});
|
|
314
341
|
});
|
|
315
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Drives the drag → mark.request path far enough to inspect the selector.
|
|
345
|
+
* `drag` is in canvas pixels; at scale 1 with a 612×792 page these are also
|
|
346
|
+
* display coordinates, since jsdom's getBoundingClientRect returns zeros.
|
|
347
|
+
*/
|
|
348
|
+
async function drawRectangle(
|
|
349
|
+
drag: { fromX: number; fromY: number; toX: number; toY: number },
|
|
350
|
+
) {
|
|
351
|
+
const request = vi.fn();
|
|
352
|
+
const session = {
|
|
353
|
+
client: { mark: { request }, beckon: { hover: vi.fn() } },
|
|
354
|
+
} as unknown as import('@semiont/sdk').SemiontSession;
|
|
355
|
+
|
|
356
|
+
render(
|
|
357
|
+
<PdfAnnotationCanvas resourceUri="res-1"
|
|
358
|
+
pdfUrl={mockPdfUrl}
|
|
359
|
+
drawingMode="rectangle"
|
|
360
|
+
selectedMotivation="highlighting"
|
|
361
|
+
session={session}
|
|
362
|
+
/>
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
await waitFor(() => {
|
|
366
|
+
const img = document.querySelector('.semiont-pdf-annotation-canvas__image');
|
|
367
|
+
expect(img).toBeInTheDocument();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const img = document.querySelector('.semiont-pdf-annotation-canvas__image') as HTMLImageElement;
|
|
371
|
+
Object.defineProperty(img, 'clientWidth', { value: 612, configurable: true });
|
|
372
|
+
Object.defineProperty(img, 'clientHeight', { value: 792, configurable: true });
|
|
373
|
+
fireEvent.load(img);
|
|
374
|
+
|
|
375
|
+
const container = document.querySelector('.semiont-pdf-annotation-canvas__container')!;
|
|
376
|
+
fireEvent.mouseDown(container, { clientX: drag.fromX, clientY: drag.fromY });
|
|
377
|
+
fireEvent.mouseMove(container, { clientX: drag.toX, clientY: drag.toY });
|
|
378
|
+
fireEvent.mouseUp(container, { clientX: drag.toX, clientY: drag.toY });
|
|
379
|
+
|
|
380
|
+
return request;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// The manual-annotation capture gap (.plans/PDF-MANUAL-ANNOTATION-TEXT.md):
|
|
384
|
+
// a hand-drawn rectangle stored geometry and nothing else, so every panel
|
|
385
|
+
// that quotes an annotation showed it blank.
|
|
386
|
+
test('a drawn rectangle carries the text it was drawn around', async () => {
|
|
387
|
+
// The "Hello world" line in canvas space: PDF y=700..712 flips to
|
|
388
|
+
// canvas y=80..92 on a 792pt page. Drag a box a little larger than it.
|
|
389
|
+
const line = pdfToCanvasCoordinates(
|
|
390
|
+
{ page: 1, x: 72, y: 700, width: 60, height: 12 }, 792, 1.0,
|
|
391
|
+
);
|
|
392
|
+
const request = await drawRectangle({
|
|
393
|
+
fromX: line.x - 2,
|
|
394
|
+
fromY: line.y - 2,
|
|
395
|
+
toX: line.x + line.width + 8,
|
|
396
|
+
toY: line.y + line.height + 4,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
await waitFor(() => expect(request).toHaveBeenCalledTimes(1));
|
|
400
|
+
|
|
401
|
+
const [source, selector, motivation] = request.mock.calls[0];
|
|
402
|
+
expect(source).toBe(resourceId('res-1'));
|
|
403
|
+
expect(motivation).toBe('highlighting');
|
|
404
|
+
expect(selector).toEqual([
|
|
405
|
+
{
|
|
406
|
+
type: 'FragmentSelector',
|
|
407
|
+
conformsTo: 'http://tools.ietf.org/rfc/rfc3778',
|
|
408
|
+
value: expect.stringContaining('page=1&viewrect='),
|
|
409
|
+
},
|
|
410
|
+
{ type: 'TextQuoteSelector', exact: 'Hello world' },
|
|
411
|
+
]);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test('a rectangle over blank space carries geometry only', async () => {
|
|
415
|
+
// Between the two lines of text — nothing under the box. An empty-string
|
|
416
|
+
// quote would assert the box was drawn around nothing, so emit no quote.
|
|
417
|
+
const request = await drawRectangle({ fromX: 40, fromY: 120, toX: 200, toY: 150 });
|
|
418
|
+
|
|
419
|
+
await waitFor(() => expect(request).toHaveBeenCalledTimes(1));
|
|
420
|
+
|
|
421
|
+
const selector = request.mock.calls[0][1];
|
|
422
|
+
expect(selector).toHaveLength(1);
|
|
423
|
+
expect(selector[0].type).toBe('FragmentSelector');
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Phase 2 of .plans/PDF-MANUAL-ANNOTATION-TEXT.md. A scanned page has no text
|
|
427
|
+
// in the browser, but the server derived one at ingest and serves it through
|
|
428
|
+
// `browse.resourceAnchoredText`. Same `AnchoredText` shape either way, so
|
|
429
|
+
// `textUnder` and the drag handler do not branch — only the source does.
|
|
430
|
+
test('a scanned page quotes from the map the server derived', async () => {
|
|
431
|
+
vi.mocked(loadPdfDocument).mockResolvedValueOnce({
|
|
432
|
+
numPages: 3,
|
|
433
|
+
getPage: vi.fn().mockResolvedValue(mockPage([])),
|
|
434
|
+
} as unknown as Awaited<ReturnType<typeof loadPdfDocument>>);
|
|
435
|
+
|
|
436
|
+
const request = vi.fn();
|
|
437
|
+
const session = {
|
|
438
|
+
client: {
|
|
439
|
+
mark: { request },
|
|
440
|
+
beckon: { hover: vi.fn() },
|
|
441
|
+
browse: {
|
|
442
|
+
resourceAnchoredText: vi.fn().mockResolvedValue({
|
|
443
|
+
text: 'Hello world again',
|
|
444
|
+
items: [
|
|
445
|
+
{ start: 0, end: 5, page: 1, x: 72, y: 700, width: 30, height: 12 },
|
|
446
|
+
{ start: 6, end: 11, page: 1, x: 106, y: 700, width: 32, height: 12 },
|
|
447
|
+
],
|
|
448
|
+
}),
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
} as unknown as import('@semiont/sdk').SemiontSession;
|
|
452
|
+
|
|
453
|
+
render(
|
|
454
|
+
<PdfAnnotationCanvas resourceUri="res-1"
|
|
455
|
+
pdfUrl={mockPdfUrl}
|
|
456
|
+
drawingMode="rectangle"
|
|
457
|
+
selectedMotivation="highlighting"
|
|
458
|
+
session={session}
|
|
459
|
+
/>
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
await waitFor(() => {
|
|
463
|
+
expect(document.querySelector('.semiont-pdf-annotation-canvas__image')).toBeInTheDocument();
|
|
464
|
+
});
|
|
465
|
+
const img = document.querySelector('.semiont-pdf-annotation-canvas__image') as HTMLImageElement;
|
|
466
|
+
Object.defineProperty(img, 'clientWidth', { value: 612, configurable: true });
|
|
467
|
+
Object.defineProperty(img, 'clientHeight', { value: 792, configurable: true });
|
|
468
|
+
fireEvent.load(img);
|
|
469
|
+
|
|
470
|
+
const line = pdfToCanvasCoordinates({ page: 1, x: 72, y: 700, width: 66, height: 12 }, 792, 1.0);
|
|
471
|
+
const container = document.querySelector('.semiont-pdf-annotation-canvas__container')!;
|
|
472
|
+
fireEvent.mouseDown(container, { clientX: line.x - 2, clientY: line.y - 2 });
|
|
473
|
+
fireEvent.mouseMove(container, { clientX: line.x + line.width + 8, clientY: line.y + line.height + 4 });
|
|
474
|
+
fireEvent.mouseUp(container, { clientX: line.x + line.width + 8, clientY: line.y + line.height + 4 });
|
|
475
|
+
|
|
476
|
+
await waitFor(() => expect(request).toHaveBeenCalledTimes(1));
|
|
477
|
+
expect(request.mock.calls[0][1]).toEqual([
|
|
478
|
+
expect.objectContaining({ type: 'FragmentSelector' }),
|
|
479
|
+
{ type: 'TextQuoteSelector', exact: 'Hello world' },
|
|
480
|
+
]);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// PERSIST-ANCHORS P4. The server map is WHOLE-RESOURCE — one artifact
|
|
484
|
+
// covering every page — so fetching it from inside the per-page load
|
|
485
|
+
// effect refetched and re-decoded the entire document's geometry on every
|
|
486
|
+
// page turn. On a 400-page scan that is the difference between one decode
|
|
487
|
+
// and one per interaction.
|
|
488
|
+
test('the whole-resource map is fetched once per document, not once per page-turn', async () => {
|
|
489
|
+
vi.mocked(loadPdfDocument).mockResolvedValueOnce({
|
|
490
|
+
numPages: 3,
|
|
491
|
+
getPage: vi.fn().mockResolvedValue(mockPage([])),
|
|
492
|
+
} as unknown as Awaited<ReturnType<typeof loadPdfDocument>>);
|
|
493
|
+
|
|
494
|
+
const resourceAnchoredText = vi.fn().mockResolvedValue({
|
|
495
|
+
text: 'Hello world again',
|
|
496
|
+
items: [
|
|
497
|
+
{ start: 0, end: 5, page: 1, x: 72, y: 700, width: 30, height: 12 },
|
|
498
|
+
{ start: 6, end: 11, page: 2, x: 106, y: 700, width: 32, height: 12 },
|
|
499
|
+
],
|
|
500
|
+
});
|
|
501
|
+
const session = {
|
|
502
|
+
client: { beckon: { hover: vi.fn() }, browse: { resourceAnchoredText } },
|
|
503
|
+
} as unknown as import('@semiont/sdk').SemiontSession;
|
|
504
|
+
|
|
505
|
+
render(
|
|
506
|
+
<PdfAnnotationCanvas resourceUri="res-1"
|
|
507
|
+
pdfUrl={mockPdfUrl}
|
|
508
|
+
drawingMode={null}
|
|
509
|
+
session={session}
|
|
510
|
+
/>
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
const user = userEvent.setup();
|
|
514
|
+
await waitFor(() => {
|
|
515
|
+
expect(screen.getByText(/page 1 of 3/i)).toBeInTheDocument();
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
await user.click(screen.getByRole('button', { name: /next/i }));
|
|
519
|
+
await waitFor(() => {
|
|
520
|
+
expect(screen.getByText(/page 2 of 3/i)).toBeInTheDocument();
|
|
521
|
+
});
|
|
522
|
+
await user.click(screen.getByRole('button', { name: /next/i }));
|
|
523
|
+
await waitFor(() => {
|
|
524
|
+
expect(screen.getByText(/page 3 of 3/i)).toBeInTheDocument();
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// The render half runs once per page load and starts alongside the
|
|
528
|
+
// anchoring half (Promise.all), so three render calls prove all three
|
|
529
|
+
// page-load effects ran to the point of resolving their map.
|
|
530
|
+
await waitFor(() => {
|
|
531
|
+
expect(vi.mocked(renderPdfPageToDataUrl)).toHaveBeenCalledTimes(3);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
expect(resourceAnchoredText).toHaveBeenCalledTimes(1);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test('a scanned page with no server map carries geometry only', async () => {
|
|
538
|
+
// Class B: pdf.js returns no runs, so the browser cannot do the job and
|
|
539
|
+
// the annotation stays geometry-only pending async enrichment (Phase 2).
|
|
540
|
+
vi.mocked(loadPdfDocument).mockResolvedValueOnce({
|
|
541
|
+
numPages: 3,
|
|
542
|
+
getPage: vi.fn().mockResolvedValue(mockPage([])),
|
|
543
|
+
} as unknown as Awaited<ReturnType<typeof loadPdfDocument>>);
|
|
544
|
+
|
|
545
|
+
const line = pdfToCanvasCoordinates(
|
|
546
|
+
{ page: 1, x: 72, y: 700, width: 60, height: 12 }, 792, 1.0,
|
|
547
|
+
);
|
|
548
|
+
const request = await drawRectangle({
|
|
549
|
+
fromX: line.x - 2,
|
|
550
|
+
fromY: line.y - 2,
|
|
551
|
+
toX: line.x + line.width + 8,
|
|
552
|
+
toY: line.y + line.height + 4,
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
await waitFor(() => expect(request).toHaveBeenCalledTimes(1));
|
|
556
|
+
|
|
557
|
+
const selector = request.mock.calls[0][1];
|
|
558
|
+
expect(selector).toHaveLength(1);
|
|
559
|
+
expect(selector[0].type).toBe('FragmentSelector');
|
|
560
|
+
});
|
|
561
|
+
|
|
316
562
|
test('accepts a drawing gesture without throwing when drawing mode is active', async () => {
|
|
317
563
|
render(
|
|
318
564
|
<PdfAnnotationCanvas resourceUri="res-1"
|
|
@@ -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
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -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
|
|