@semiont/react-ui 0.5.19 → 0.5.21
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-75GY2EDR.js → PdfAnnotationCanvas.client-V7Q3BEEQ.js} +42 -45
- package/dist/PdfAnnotationCanvas.client-V7Q3BEEQ.js.map +1 -0
- package/dist/index.css +9 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +231 -55
- package/dist/index.js +2115 -2161
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/components/AssistProgress.tsx +170 -0
- package/src/components/__tests__/AssistProgress.test.tsx +164 -0
- package/src/components/annotation/__tests__/AnnotateToolbar.display.test.tsx +11 -1
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +23 -54
- package/src/components/pdf-annotation/__tests__/rects-for-page.test.ts +96 -0
- package/src/components/pdf-annotation/rects-for-page.ts +47 -0
- package/src/components/resource/ResourceViewer.tsx +23 -55
- package/src/components/resource/panels/AssessmentPanel.tsx +1 -1
- package/src/components/resource/panels/AssistSection.tsx +111 -180
- package/src/components/resource/panels/AssistShell.tsx +77 -0
- package/src/components/resource/panels/CommentsPanel.tsx +1 -1
- package/src/components/resource/panels/HighlightPanel.tsx +1 -1
- package/src/components/resource/panels/ReferenceEntry.tsx +8 -1
- package/src/components/resource/panels/ReferencesPanel.tsx +101 -132
- package/src/components/resource/panels/TaggingPanel.tsx +23 -86
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +6 -14
- package/src/components/resource/panels/__tests__/AssistShell.test.tsx +59 -0
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +20 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +14 -20
- package/src/components/toolbar/Toolbar.css +13 -1
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +30 -0
- package/src/features/resource-compose/components/ResourceComposePage.tsx +19 -1
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +24 -64
- package/src/styles/features/compose.css +15 -0
- package/dist/PdfAnnotationCanvas.client-75GY2EDR.js.map +0 -1
- package/src/components/AnnotateReferencesProgressWidget.tsx +0 -125
- package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +0 -101
|
@@ -5,7 +5,7 @@ import { useTranslations } from '../../../contexts/TranslationContext';
|
|
|
5
5
|
import type { SemiontSession } from '@semiont/sdk';
|
|
6
6
|
import { useSessionEventSubscriptions } from '../../../hooks/useSessionEventSubscriptions';
|
|
7
7
|
import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
|
|
8
|
-
import {
|
|
8
|
+
import { AssistShell } from './AssistShell';
|
|
9
9
|
import { ReferenceEntry } from './ReferenceEntry';
|
|
10
10
|
import type { components, Selector } from '@semiont/core';
|
|
11
11
|
import { getTextPositionSelector, getTargetSelector } from '@semiont/core';
|
|
@@ -109,19 +109,6 @@ export function ReferencesPanel({
|
|
|
109
109
|
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
|
|
110
110
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
111
111
|
|
|
112
|
-
// Collapsible detection section state - load from localStorage, default expanded
|
|
113
|
-
const [isAssistExpanded, setIsDetectExpanded] = useState(() => {
|
|
114
|
-
if (typeof window === 'undefined') return true;
|
|
115
|
-
const stored = localStorage.getItem('assist-section-expanded-reference');
|
|
116
|
-
return stored ? stored === 'true' : true;
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// Persist detection section expanded state to localStorage
|
|
120
|
-
useEffect(() => {
|
|
121
|
-
if (typeof window === 'undefined') return;
|
|
122
|
-
localStorage.setItem('assist-section-expanded-reference', String(isAssistExpanded));
|
|
123
|
-
}, [isAssistExpanded]);
|
|
124
|
-
|
|
125
112
|
// Direct ref management - replace useAnnotationPanel hook
|
|
126
113
|
const entryRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
|
127
114
|
|
|
@@ -351,120 +338,17 @@ export function ReferencesPanel({
|
|
|
351
338
|
|
|
352
339
|
{/* Scrollable content area */}
|
|
353
340
|
<div ref={containerRef} className="semiont-panel__content">
|
|
354
|
-
{/* Assist Section - only in Annotate mode
|
|
341
|
+
{/* Assist Section - only in Annotate mode; shown for any media type (AI detection is media-agnostic — text is resolved via the media-type registry, incl. PDF text layers) */}
|
|
355
342
|
{annotateMode && (
|
|
356
|
-
<
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
›
|
|
366
|
-
</span>
|
|
367
|
-
</button>
|
|
368
|
-
{isAssistExpanded && (
|
|
369
|
-
<>
|
|
370
|
-
{/* Show annotation UI when not actively assisting */}
|
|
371
|
-
{!isAssisting && (
|
|
372
|
-
<div className="semiont-assist-widget" data-type="reference">
|
|
373
|
-
<>
|
|
374
|
-
{/* Completed annotation log - shown after completion */}
|
|
375
|
-
{lastAnnotationLog && lastAnnotationLog.length > 0 && (
|
|
376
|
-
<div className="semiont-assist-widget__log">
|
|
377
|
-
<div className="semiont-assist-widget__log-items">
|
|
378
|
-
{lastAnnotationLog.map((item, index) => (
|
|
379
|
-
<div key={index} className="semiont-assist-widget__log-item">
|
|
380
|
-
<span className="semiont-assist-widget__log-check">✓</span>
|
|
381
|
-
<span className="semiont-assist-widget__log-type">{item.entityType}:</span>
|
|
382
|
-
<span>{t('found', { count: item.foundCount })}</span>
|
|
383
|
-
</div>
|
|
384
|
-
))}
|
|
385
|
-
</div>
|
|
386
|
-
</div>
|
|
387
|
-
)}
|
|
388
|
-
|
|
389
|
-
{/* Entity Types Selection */}
|
|
390
|
-
<div className="semiont-assist-widget__entity-types">
|
|
391
|
-
<p className="semiont-assist-widget__label">
|
|
392
|
-
{t('selectEntityTypes')}
|
|
393
|
-
</p>
|
|
394
|
-
<div className="semiont-assist-widget__chips">
|
|
395
|
-
{allEntityTypes.length > 0 ? (
|
|
396
|
-
allEntityTypes.map((type: string) => (
|
|
397
|
-
<button
|
|
398
|
-
key={type}
|
|
399
|
-
onClick={() => {
|
|
400
|
-
setSelectedEntityTypes(prev =>
|
|
401
|
-
prev.includes(type)
|
|
402
|
-
? prev.filter(t => t !== type)
|
|
403
|
-
: [...prev, type]
|
|
404
|
-
);
|
|
405
|
-
}}
|
|
406
|
-
aria-pressed={selectedEntityTypes.includes(type)}
|
|
407
|
-
aria-label={`${selectedEntityTypes.includes(type) ? t('deselect') : t('select')} ${type}`}
|
|
408
|
-
className="semiont-chip semiont-chip--selectable"
|
|
409
|
-
data-selected={selectedEntityTypes.includes(type)}
|
|
410
|
-
>
|
|
411
|
-
{type}
|
|
412
|
-
</button>
|
|
413
|
-
))
|
|
414
|
-
) : (
|
|
415
|
-
<p className="semiont-assist-widget__no-types">
|
|
416
|
-
{t('noEntityTypes')}
|
|
417
|
-
</p>
|
|
418
|
-
)}
|
|
419
|
-
</div>
|
|
420
|
-
</div>
|
|
421
|
-
|
|
422
|
-
{/* Selected Count */}
|
|
423
|
-
{selectedEntityTypes.length > 0 && (
|
|
424
|
-
<p className="semiont-assist-widget__count">
|
|
425
|
-
{t('typesSelected', { count: selectedEntityTypes.length })}
|
|
426
|
-
</p>
|
|
427
|
-
)}
|
|
428
|
-
|
|
429
|
-
{/* Include Descriptive References Checkbox */}
|
|
430
|
-
<div className="semiont-assist-widget__checkbox-group">
|
|
431
|
-
<label className="semiont-assist-widget__checkbox-label">
|
|
432
|
-
<input
|
|
433
|
-
type="checkbox"
|
|
434
|
-
checked={includeDescriptiveReferences}
|
|
435
|
-
onChange={(e) => setIncludeDescriptiveReferences(e.target.checked)}
|
|
436
|
-
className="semiont-assist-widget__checkbox"
|
|
437
|
-
/>
|
|
438
|
-
<span>{t('includeDescriptiveReferences')}</span>
|
|
439
|
-
</label>
|
|
440
|
-
<p className="semiont-assist-widget__checkbox-hint">
|
|
441
|
-
{t('descriptiveReferencesTooltip')}
|
|
442
|
-
</p>
|
|
443
|
-
</div>
|
|
444
|
-
|
|
445
|
-
{/* Start Assist Button */}
|
|
446
|
-
<button
|
|
447
|
-
onClick={handleAssist}
|
|
448
|
-
disabled={selectedEntityTypes.length === 0}
|
|
449
|
-
title={t('annotate')}
|
|
450
|
-
className="semiont-button"
|
|
451
|
-
data-variant="assist"
|
|
452
|
-
data-type="reference"
|
|
453
|
-
>
|
|
454
|
-
<span className="semiont-button-icon">✨</span>
|
|
455
|
-
<span>{t('annotate')}</span>
|
|
456
|
-
</button>
|
|
457
|
-
</>
|
|
458
|
-
</div>
|
|
459
|
-
)}
|
|
460
|
-
|
|
461
|
-
{/* Annotation Progress - shown when active */}
|
|
462
|
-
{isAssisting && progress && (
|
|
463
|
-
<AnnotateReferencesProgressWidget
|
|
464
|
-
progress={progress}
|
|
465
|
-
annotationType="reference"
|
|
466
|
-
cancelJobType="annotation"
|
|
467
|
-
translations={{
|
|
343
|
+
<AssistShell
|
|
344
|
+
assistType="reference"
|
|
345
|
+
title={t('annotateReferences')}
|
|
346
|
+
isAssisting={isAssisting}
|
|
347
|
+
progress={progress}
|
|
348
|
+
progressProps={{
|
|
349
|
+
onCancel: () => session?.client.job.cancelRequest('annotation'),
|
|
350
|
+
onDismiss: () => session?.client.mark.dismissProgress(),
|
|
351
|
+
translations: {
|
|
468
352
|
title: t('annotationProgressTitle'),
|
|
469
353
|
cancel: t('cancelAnnotation'),
|
|
470
354
|
inProgress: t('annotating'),
|
|
@@ -472,12 +356,97 @@ export function ReferencesPanel({
|
|
|
472
356
|
failed: t('failed'),
|
|
473
357
|
found: (count) => t('found', { count }),
|
|
474
358
|
current: (entityType) => t('current', { entityType }),
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
359
|
+
close: t('closeProgress'),
|
|
360
|
+
},
|
|
361
|
+
}}
|
|
362
|
+
form={
|
|
363
|
+
<>
|
|
364
|
+
{/* Completed annotation log - shown after completion */}
|
|
365
|
+
{lastAnnotationLog && lastAnnotationLog.length > 0 && (
|
|
366
|
+
<div className="semiont-assist-widget__log">
|
|
367
|
+
<div className="semiont-assist-widget__log-items">
|
|
368
|
+
{lastAnnotationLog.map((item, index) => (
|
|
369
|
+
<div key={index} className="semiont-assist-widget__log-item">
|
|
370
|
+
<span className="semiont-assist-widget__log-check">✓</span>
|
|
371
|
+
<span className="semiont-assist-widget__log-type">{item.entityType}:</span>
|
|
372
|
+
<span>{t('found', { count: item.foundCount })}</span>
|
|
373
|
+
</div>
|
|
374
|
+
))}
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
)}
|
|
378
|
+
|
|
379
|
+
{/* Entity Types Selection */}
|
|
380
|
+
<div className="semiont-assist-widget__entity-types">
|
|
381
|
+
<p className="semiont-assist-widget__label">
|
|
382
|
+
{t('selectEntityTypes')}
|
|
383
|
+
</p>
|
|
384
|
+
<div className="semiont-assist-widget__chips">
|
|
385
|
+
{allEntityTypes.length > 0 ? (
|
|
386
|
+
allEntityTypes.map((type: string) => (
|
|
387
|
+
<button
|
|
388
|
+
key={type}
|
|
389
|
+
onClick={() => {
|
|
390
|
+
setSelectedEntityTypes(prev =>
|
|
391
|
+
prev.includes(type)
|
|
392
|
+
? prev.filter(t => t !== type)
|
|
393
|
+
: [...prev, type]
|
|
394
|
+
);
|
|
395
|
+
}}
|
|
396
|
+
aria-pressed={selectedEntityTypes.includes(type)}
|
|
397
|
+
aria-label={`${selectedEntityTypes.includes(type) ? t('deselect') : t('select')} ${type}`}
|
|
398
|
+
className="semiont-chip semiont-chip--selectable"
|
|
399
|
+
data-selected={selectedEntityTypes.includes(type)}
|
|
400
|
+
>
|
|
401
|
+
{type}
|
|
402
|
+
</button>
|
|
403
|
+
))
|
|
404
|
+
) : (
|
|
405
|
+
<p className="semiont-assist-widget__no-types">
|
|
406
|
+
{t('noEntityTypes')}
|
|
407
|
+
</p>
|
|
408
|
+
)}
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
|
|
412
|
+
{/* Selected Count */}
|
|
413
|
+
{selectedEntityTypes.length > 0 && (
|
|
414
|
+
<p className="semiont-assist-widget__count">
|
|
415
|
+
{t('typesSelected', { count: selectedEntityTypes.length })}
|
|
416
|
+
</p>
|
|
417
|
+
)}
|
|
418
|
+
|
|
419
|
+
{/* Include Descriptive References Checkbox */}
|
|
420
|
+
<div className="semiont-assist-widget__checkbox-group">
|
|
421
|
+
<label className="semiont-assist-widget__checkbox-label">
|
|
422
|
+
<input
|
|
423
|
+
type="checkbox"
|
|
424
|
+
checked={includeDescriptiveReferences}
|
|
425
|
+
onChange={(e) => setIncludeDescriptiveReferences(e.target.checked)}
|
|
426
|
+
className="semiont-assist-widget__checkbox"
|
|
427
|
+
/>
|
|
428
|
+
<span>{t('includeDescriptiveReferences')}</span>
|
|
429
|
+
</label>
|
|
430
|
+
<p className="semiont-assist-widget__checkbox-hint">
|
|
431
|
+
{t('descriptiveReferencesTooltip')}
|
|
432
|
+
</p>
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
{/* Start Assist Button */}
|
|
436
|
+
<button
|
|
437
|
+
onClick={handleAssist}
|
|
438
|
+
disabled={selectedEntityTypes.length === 0}
|
|
439
|
+
title={t('annotate')}
|
|
440
|
+
className="semiont-button"
|
|
441
|
+
data-variant="assist"
|
|
442
|
+
data-type="reference"
|
|
443
|
+
>
|
|
444
|
+
<span className="semiont-button-icon">✨</span>
|
|
445
|
+
<span>{t('annotate')}</span>
|
|
446
|
+
</button>
|
|
478
447
|
</>
|
|
479
|
-
|
|
480
|
-
|
|
448
|
+
}
|
|
449
|
+
/>
|
|
481
450
|
)}
|
|
482
451
|
|
|
483
452
|
{/* References List Section */}
|
|
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
5
|
import { useObservable } from '../../../hooks/useObservable';
|
|
6
6
|
import type { SemiontSession } from '@semiont/sdk';
|
|
7
|
+
import { AssistShell } from './AssistShell';
|
|
7
8
|
import { useSessionEventSubscriptions } from '../../../hooks/useSessionEventSubscriptions';
|
|
8
9
|
import type { components, Selector } from '@semiont/core';
|
|
9
10
|
import { getTextPositionSelector, getTargetSelector } from '@semiont/core';
|
|
@@ -111,19 +112,6 @@ export function TaggingPanel({
|
|
|
111
112
|
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
|
|
112
113
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
113
114
|
|
|
114
|
-
// Collapsible detection section state - load from localStorage, default expanded
|
|
115
|
-
const [isAssistExpanded, setIsDetectExpanded] = useState(() => {
|
|
116
|
-
if (typeof window === 'undefined') return true;
|
|
117
|
-
const stored = localStorage.getItem('assist-section-expanded-tag');
|
|
118
|
-
return stored ? stored === 'true' : true;
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// Persist detection section expanded state to localStorage
|
|
122
|
-
useEffect(() => {
|
|
123
|
-
if (typeof window === 'undefined') return;
|
|
124
|
-
localStorage.setItem('assist-section-expanded-tag', String(isAssistExpanded));
|
|
125
|
-
}, [isAssistExpanded]);
|
|
126
|
-
|
|
127
115
|
// Subscribe to click events - update focused state
|
|
128
116
|
// Event handler for annotation clicks (extracted to avoid inline arrow function)
|
|
129
117
|
const handleAnnotationClick = useCallback(({ annotationId }: { annotationId: string }) => {
|
|
@@ -357,22 +345,14 @@ export function TaggingPanel({
|
|
|
357
345
|
|
|
358
346
|
{/* Assist Section - only in Annotate mode */}
|
|
359
347
|
{annotateMode && (
|
|
360
|
-
<
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
<span className="semiont-panel__section-chevron" data-expanded={isAssistExpanded}>
|
|
369
|
-
›
|
|
370
|
-
</span>
|
|
371
|
-
</button>
|
|
372
|
-
{isAssistExpanded && (
|
|
373
|
-
<div className="semiont-assist-widget" data-assisting={isAssisting && progress ? 'true' : 'false'} data-type="tag">
|
|
374
|
-
{!isAssisting && !progress && (
|
|
375
|
-
<>
|
|
348
|
+
<AssistShell
|
|
349
|
+
assistType="tag"
|
|
350
|
+
title={t('annotateTags')}
|
|
351
|
+
isAssisting={isAssisting}
|
|
352
|
+
progress={progress}
|
|
353
|
+
progressProps={{ showPercentBar: true }}
|
|
354
|
+
form={
|
|
355
|
+
<>
|
|
376
356
|
{/* Empty-state — registry has resolved with no schemas. */}
|
|
377
357
|
{noSchemasRegistered && (
|
|
378
358
|
<p className="semiont-form__help" data-type="tag-no-schemas">
|
|
@@ -461,64 +441,21 @@ export function TaggingPanel({
|
|
|
461
441
|
</p>
|
|
462
442
|
</div>
|
|
463
443
|
)}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
<span className="semiont-button-icon">✨</span>
|
|
476
|
-
<span>{t('annotate')}</span>
|
|
477
|
-
</button>
|
|
478
|
-
|
|
479
|
-
{/* Annotation Progress */}
|
|
480
|
-
{isAssisting && progress && (
|
|
481
|
-
<div className="semiont-annotation-progress" data-type="tag">
|
|
482
|
-
{/* Request Parameters */}
|
|
483
|
-
{progress.requestParams && progress.requestParams.length > 0 && (
|
|
484
|
-
<div className="semiont-annotation-progress__params" data-type="tag">
|
|
485
|
-
<div className="semiont-annotation-progress__params-title">Request Parameters:</div>
|
|
486
|
-
{progress.requestParams.map((param, idx) => (
|
|
487
|
-
<div key={idx} className="semiont-annotation-progress__param">
|
|
488
|
-
<span className="semiont-annotation-progress__param-label">{param.label}:</span> {param.value}
|
|
489
|
-
</div>
|
|
490
|
-
))}
|
|
491
|
-
</div>
|
|
492
|
-
)}
|
|
444
|
+
{/* Assist Button */}
|
|
445
|
+
<button
|
|
446
|
+
onClick={handleAssist}
|
|
447
|
+
disabled={selectedCategories.size === 0 || isAssisting}
|
|
448
|
+
className="semiont-button"
|
|
449
|
+
data-variant="assist"
|
|
450
|
+
data-type="tag"
|
|
451
|
+
>
|
|
452
|
+
<span className="semiont-button-icon">✨</span>
|
|
453
|
+
<span>{t('annotate')}</span>
|
|
454
|
+
</button>
|
|
493
455
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
<span>{progress.message}</span>
|
|
498
|
-
</div>
|
|
499
|
-
{progress.currentCategory && (
|
|
500
|
-
<div className="semiont-annotation-progress__details">
|
|
501
|
-
Processing: {progress.currentCategory}
|
|
502
|
-
{progress.processedCategories !== undefined && progress.totalCategories !== undefined && (
|
|
503
|
-
<> ({progress.processedCategories}/{progress.totalCategories})</>
|
|
504
|
-
)}
|
|
505
|
-
</div>
|
|
506
|
-
)}
|
|
507
|
-
</div>
|
|
508
|
-
{progress.percentage !== undefined && (
|
|
509
|
-
<div className="semiont-progress-bar">
|
|
510
|
-
<div
|
|
511
|
-
className="semiont-progress-bar__fill"
|
|
512
|
-
data-type="tag"
|
|
513
|
-
style={{ width: `${progress.percentage}%` }}
|
|
514
|
-
/>
|
|
515
|
-
</div>
|
|
516
|
-
)}
|
|
517
|
-
</div>
|
|
518
|
-
)}
|
|
519
|
-
</div>
|
|
520
|
-
)}
|
|
521
|
-
</div>
|
|
456
|
+
</>
|
|
457
|
+
}
|
|
458
|
+
/>
|
|
522
459
|
)}
|
|
523
460
|
|
|
524
461
|
{/* Tags list */}
|
|
@@ -6,7 +6,7 @@ import type { components, Selector } from '@semiont/core';
|
|
|
6
6
|
type JobProgress = components['schemas']['JobProgress'];
|
|
7
7
|
import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
|
|
8
8
|
import type { SemiontSession } from '@semiont/sdk';
|
|
9
|
-
import type
|
|
9
|
+
import { annotatorKeyForMotivation, type Annotator } from '../../../lib/annotation-registry';
|
|
10
10
|
import { StatisticsPanel } from './StatisticsPanel';
|
|
11
11
|
import { HighlightPanel } from './HighlightPanel';
|
|
12
12
|
import { ReferencesPanel } from './ReferencesPanel';
|
|
@@ -154,21 +154,13 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
|
|
|
154
154
|
}
|
|
155
155
|
}, [props.initialTabGeneration]); // Only watch generation counter, not the tab itself
|
|
156
156
|
|
|
157
|
-
// Auto-switch to the appropriate tab when creating a new annotation
|
|
157
|
+
// Auto-switch to the appropriate tab when creating a new annotation. Tab
|
|
158
|
+
// keys are the annotator keys — derived from the registry, the single
|
|
159
|
+
// motivation↔annotator source, so this can't drift from ANNOTATORS.
|
|
158
160
|
useEffect(() => {
|
|
159
161
|
if (props.pendingAnnotation) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
'linking': 'reference',
|
|
163
|
-
'commenting': 'comment',
|
|
164
|
-
'tagging': 'tag',
|
|
165
|
-
'assessing': 'assessment',
|
|
166
|
-
'highlighting': 'highlight'
|
|
167
|
-
};
|
|
168
|
-
const tab = motivationToTab[props.pendingAnnotation.motivation];
|
|
169
|
-
if (tab) {
|
|
170
|
-
setActiveTab(tab);
|
|
171
|
-
}
|
|
162
|
+
const tab = annotatorKeyForMotivation(props.pendingAnnotation.motivation);
|
|
163
|
+
if (tab) setActiveTab(tab);
|
|
172
164
|
}
|
|
173
165
|
}, [props.pendingAnnotation]);
|
|
174
166
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AssistShell (#7) — the shared assist chrome. Pins the net behavior that
|
|
3
|
+
* holds across the isAssisting-prop relocation: the form/progress switch and
|
|
4
|
+
* the dismiss policy (dismiss is offered only once the assist is no longer
|
|
5
|
+
* running — the SHELL owns that policy; AssistProgress just renders whatever
|
|
6
|
+
* callback it is handed).
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
9
|
+
import { render, screen } from '@testing-library/react';
|
|
10
|
+
import userEvent from '@testing-library/user-event';
|
|
11
|
+
import '@testing-library/jest-dom';
|
|
12
|
+
import { AssistShell } from '../AssistShell';
|
|
13
|
+
|
|
14
|
+
const progress = { stage: 'analyzing', percentage: 50, message: 'working' };
|
|
15
|
+
|
|
16
|
+
describe('AssistShell', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
localStorage.clear();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('renders the form when there is no progress, the progress when there is', () => {
|
|
22
|
+
const { rerender } = render(
|
|
23
|
+
<AssistShell assistType="tag" title="Annotate Tags" isAssisting={false} progress={null}
|
|
24
|
+
form={<button type="button">the form</button>} />,
|
|
25
|
+
);
|
|
26
|
+
expect(screen.getByText('the form')).toBeInTheDocument();
|
|
27
|
+
rerender(
|
|
28
|
+
<AssistShell assistType="tag" title="Annotate Tags" isAssisting={true} progress={progress}
|
|
29
|
+
form={<button type="button">the form</button>} />,
|
|
30
|
+
);
|
|
31
|
+
expect(screen.queryByText('the form')).not.toBeInTheDocument();
|
|
32
|
+
expect(screen.getByText('working')).toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('withholds dismiss while assisting, offers it once terminal', async () => {
|
|
36
|
+
const onDismiss = vi.fn();
|
|
37
|
+
const props = {
|
|
38
|
+
assistType: 'highlight', title: 'Annotate Highlights', progress,
|
|
39
|
+
form: <span>form</span>,
|
|
40
|
+
progressProps: { onDismiss, translations: { close: 'Close' } },
|
|
41
|
+
};
|
|
42
|
+
const { rerender } = render(<AssistShell {...props} isAssisting={true} />);
|
|
43
|
+
expect(screen.queryByLabelText('Close')).not.toBeInTheDocument();
|
|
44
|
+
|
|
45
|
+
rerender(<AssistShell {...props} isAssisting={false} />);
|
|
46
|
+
await userEvent.click(screen.getByLabelText('Close'));
|
|
47
|
+
expect(onDismiss).toHaveBeenCalledOnce();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('persists the expand state per assist type', async () => {
|
|
51
|
+
render(
|
|
52
|
+
<AssistShell assistType="reference" title="Annotate References" isAssisting={false} progress={null}
|
|
53
|
+
form={<span>form</span>} />,
|
|
54
|
+
);
|
|
55
|
+
await userEvent.click(screen.getByRole('button', { name: /Annotate References/ }));
|
|
56
|
+
expect(screen.queryByText('form')).not.toBeInTheDocument();
|
|
57
|
+
expect(localStorage.getItem('assist-section-expanded-reference')).toBe('false');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -306,6 +306,26 @@ describe('ReferenceEntry', () => {
|
|
|
306
306
|
|
|
307
307
|
bindSpy.mockRestore();
|
|
308
308
|
});
|
|
309
|
+
|
|
310
|
+
it('emits bind:body-error (resource-stamped, client-local) when unlink fails', async () => {
|
|
311
|
+
// This component has no toast surface — its catch emits the client-local
|
|
312
|
+
// bind error and useOutcomeToasts surfaces it. The raw
|
|
313
|
+
// bind:body-update-failed wire reply is busRequest plumbing, not UI.
|
|
314
|
+
mockIsBodyResolved.mockReturnValue(true);
|
|
315
|
+
mockGetBodySource.mockReturnValue('linked-doc');
|
|
316
|
+
|
|
317
|
+
const bindSpy = vi.spyOn(BindNamespace.prototype, 'body').mockRejectedValue(new Error('link is load-bearing'));
|
|
318
|
+
const errors: unknown[] = [];
|
|
319
|
+
eventBus.get('bind:body-error').subscribe(e => errors.push(e));
|
|
320
|
+
|
|
321
|
+
const { container } = renderEntry({ annotateMode: true });
|
|
322
|
+
await userEvent.click(container.querySelector('.semiont-reference-unlink')!);
|
|
323
|
+
|
|
324
|
+
await vi.waitFor(() => expect(errors).toHaveLength(1));
|
|
325
|
+
expect(errors[0]).toEqual({ resourceId: 'resource-1', message: 'link is load-bearing' });
|
|
326
|
+
|
|
327
|
+
bindSpy.mockRestore();
|
|
328
|
+
});
|
|
309
329
|
});
|
|
310
330
|
|
|
311
331
|
describe('Status icon — stub reference', () => {
|
|
@@ -77,16 +77,6 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
|
|
|
77
77
|
TranslationProvider: ({ children }: { children: React.ReactNode }) => children,
|
|
78
78
|
}));
|
|
79
79
|
|
|
80
|
-
// Mock AnnotateReferencesProgressWidget - simplified to avoid module import issues
|
|
81
|
-
vi.mock('@/components/AnnotateReferencesProgressWidget', () => ({
|
|
82
|
-
AnnotateReferencesProgressWidget: ({ progress }: any) => (
|
|
83
|
-
<div data-testid="annotation-progress-widget">
|
|
84
|
-
<div data-testid="progress-data">{JSON.stringify(progress)}</div>
|
|
85
|
-
<button title="Cancel Annotation">Cancel</button>
|
|
86
|
-
</div>
|
|
87
|
-
),
|
|
88
|
-
}));
|
|
89
|
-
|
|
90
80
|
describe('ReferencesPanel Component', () => {
|
|
91
81
|
// Mock Link component
|
|
92
82
|
const MockLink = ({ href, children, ...props }: any) => (
|
|
@@ -392,7 +382,7 @@ describe('ReferencesPanel Component', () => {
|
|
|
392
382
|
/>
|
|
393
383
|
);
|
|
394
384
|
|
|
395
|
-
expect(screen.
|
|
385
|
+
expect(screen.getByText('Detecting references...')).toBeInTheDocument();
|
|
396
386
|
});
|
|
397
387
|
|
|
398
388
|
it('should pass progress data to widget', () => {
|
|
@@ -414,9 +404,9 @@ describe('ReferencesPanel Component', () => {
|
|
|
414
404
|
/>
|
|
415
405
|
);
|
|
416
406
|
|
|
417
|
-
|
|
418
|
-
expect(
|
|
419
|
-
expect(
|
|
407
|
+
// Real AssistProgress renders the completed entity-type log.
|
|
408
|
+
expect(screen.getByText('Person:')).toBeInTheDocument();
|
|
409
|
+
expect(screen.getByText('Organization:')).toBeInTheDocument();
|
|
420
410
|
});
|
|
421
411
|
|
|
422
412
|
it('should hide entity type selection during detection', () => {
|
|
@@ -441,7 +431,9 @@ describe('ReferencesPanel Component', () => {
|
|
|
441
431
|
/>
|
|
442
432
|
);
|
|
443
433
|
|
|
444
|
-
|
|
434
|
+
// Real AssistProgress titles the cancel button with t('cancelAnnotation')
|
|
435
|
+
// (the mock translator echoes unknown keys).
|
|
436
|
+
const cancelButton = screen.getByTitle('cancelAnnotation');
|
|
445
437
|
expect(cancelButton).toBeInTheDocument();
|
|
446
438
|
});
|
|
447
439
|
});
|
|
@@ -577,9 +569,11 @@ describe('ReferencesPanel Component', () => {
|
|
|
577
569
|
/>
|
|
578
570
|
);
|
|
579
571
|
|
|
580
|
-
// Should not show any log items
|
|
572
|
+
// Should not show any log items. Terminal progress (dismissable) is
|
|
573
|
+
// shown instead of the form — the AssistShell normalization (#7); the
|
|
574
|
+
// form returns once progress clears.
|
|
581
575
|
expect(screen.queryByText('✓')).not.toBeInTheDocument();
|
|
582
|
-
expect(screen.
|
|
576
|
+
expect(screen.queryByText('Select entity types')).not.toBeInTheDocument();
|
|
583
577
|
});
|
|
584
578
|
});
|
|
585
579
|
|
|
@@ -600,7 +594,7 @@ describe('ReferencesPanel Component', () => {
|
|
|
600
594
|
);
|
|
601
595
|
|
|
602
596
|
// Detecting state
|
|
603
|
-
expect(screen.
|
|
597
|
+
expect(screen.getByText('Detecting references...')).toBeInTheDocument();
|
|
604
598
|
expect(screen.queryByText('Select entity types')).not.toBeInTheDocument();
|
|
605
599
|
});
|
|
606
600
|
|
|
@@ -614,7 +608,7 @@ describe('ReferencesPanel Component', () => {
|
|
|
614
608
|
);
|
|
615
609
|
|
|
616
610
|
// Detecting
|
|
617
|
-
expect(screen.
|
|
611
|
+
expect(screen.getByText('Detecting references...')).toBeInTheDocument();
|
|
618
612
|
|
|
619
613
|
// Complete - first trigger useEffect to copy to lastDetectionLog
|
|
620
614
|
rerender(
|
|
@@ -635,7 +629,7 @@ describe('ReferencesPanel Component', () => {
|
|
|
635
629
|
<ReferencesPanel {...panelProps()} isAssisting={false} progress={null} />
|
|
636
630
|
);
|
|
637
631
|
|
|
638
|
-
expect(screen.
|
|
632
|
+
expect(screen.queryByText('Annotation complete')).not.toBeInTheDocument();
|
|
639
633
|
// Both log and selection UI should be visible
|
|
640
634
|
expect(screen.getByText('Person:')).toBeInTheDocument();
|
|
641
635
|
expect(screen.getByText('Select entity types')).toBeInTheDocument();
|
|
@@ -230,6 +230,18 @@
|
|
|
230
230
|
border-bottom: none;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/* Icon-only collapsed triggers — but the label stays in the accessibility
|
|
234
|
+
tree rather than `display: none`. Its sibling is a bare emoji span with no
|
|
235
|
+
accessible name, so hiding the label outright would leave the trigger
|
|
236
|
+
unnamed for screen readers. Same technique as .semiont-sr-only. */
|
|
233
237
|
.semiont-annotate-toolbar--compact .semiont-dropdown-label {
|
|
234
|
-
|
|
238
|
+
position: absolute;
|
|
239
|
+
width: 1px;
|
|
240
|
+
height: 1px;
|
|
241
|
+
padding: 0;
|
|
242
|
+
margin: -1px;
|
|
243
|
+
overflow: hidden;
|
|
244
|
+
clip: rect(0, 0, 0, 0);
|
|
245
|
+
white-space: nowrap;
|
|
246
|
+
border-width: 0;
|
|
235
247
|
}
|