@semiont/react-ui 0.2.33-build.78 → 0.2.33-build.80
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/EventBusContext-7GvDyO0d.d.mts +414 -0
- package/dist/{PdfAnnotationCanvas.client-ADC4FFSE.mjs → PdfAnnotationCanvas.client-RAJRPQLU.mjs} +42 -27
- package/dist/PdfAnnotationCanvas.client-RAJRPQLU.mjs.map +1 -0
- package/dist/{ar-RNNSPLQB.mjs → ar-4ZEORRW2.mjs} +8 -4
- package/dist/ar-4ZEORRW2.mjs.map +1 -0
- package/dist/{bn-S2CDL7EC.mjs → bn-SEDE5BQJ.mjs} +8 -4
- package/dist/bn-SEDE5BQJ.mjs.map +1 -0
- package/dist/{chunk-UDX2Q35T.mjs → chunk-D7NBW4RV.mjs} +8 -4
- package/dist/chunk-D7NBW4RV.mjs.map +1 -0
- package/dist/{chunk-35LLVRFK.mjs → chunk-ZR4ZV2LY.mjs} +206 -146
- package/dist/chunk-ZR4ZV2LY.mjs.map +1 -0
- package/dist/{cs-RSV675WU.mjs → cs-7W4WF5WD.mjs} +8 -4
- package/dist/cs-7W4WF5WD.mjs.map +1 -0
- package/dist/{da-CHXNPWJC.mjs → da-75XGBCBK.mjs} +8 -4
- package/dist/da-75XGBCBK.mjs.map +1 -0
- package/dist/{de-KPEZ53D4.mjs → de-ODJVFLHM.mjs} +8 -4
- package/dist/de-ODJVFLHM.mjs.map +1 -0
- package/dist/{el-MW2BME5T.mjs → el-C4PM4WB3.mjs} +8 -4
- package/dist/el-C4PM4WB3.mjs.map +1 -0
- package/dist/{en-EVMIX24Y.mjs → en-KJCJQ4OO.mjs} +2 -2
- package/dist/{es-HQ24NYS3.mjs → es-WD33R7QL.mjs} +8 -4
- package/dist/es-WD33R7QL.mjs.map +1 -0
- package/dist/{fa-W34LRLHG.mjs → fa-2BP6V56P.mjs} +8 -4
- package/dist/fa-2BP6V56P.mjs.map +1 -0
- package/dist/{fi-3U44IGOA.mjs → fi-USRRW24J.mjs} +8 -4
- package/dist/fi-USRRW24J.mjs.map +1 -0
- package/dist/{fr-N7DKX6NN.mjs → fr-EC5S6WVF.mjs} +8 -4
- package/dist/fr-EC5S6WVF.mjs.map +1 -0
- package/dist/{he-CS4WRXN3.mjs → he-7TBVIKAA.mjs} +8 -4
- package/dist/he-7TBVIKAA.mjs.map +1 -0
- package/dist/{hi-GJDY46KA.mjs → hi-FO4VIZLA.mjs} +8 -4
- package/dist/hi-FO4VIZLA.mjs.map +1 -0
- package/dist/{id-WAEZJK2Y.mjs → id-7U7GGVWY.mjs} +8 -4
- package/dist/id-7U7GGVWY.mjs.map +1 -0
- package/dist/index.css +123 -85
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +699 -529
- package/dist/index.mjs +4291 -3491
- package/dist/index.mjs.map +1 -1
- package/dist/{it-VDNDMZPU.mjs → it-Y4OPL6I2.mjs} +8 -4
- package/dist/it-Y4OPL6I2.mjs.map +1 -0
- package/dist/{ja-5PEH56J5.mjs → ja-PK7SQL55.mjs} +8 -4
- package/dist/ja-PK7SQL55.mjs.map +1 -0
- package/dist/{ko-JYPL3WVA.mjs → ko-L25PXMYD.mjs} +8 -4
- package/dist/ko-L25PXMYD.mjs.map +1 -0
- package/dist/{ms-5PZVW76T.mjs → ms-STH777QM.mjs} +8 -4
- package/dist/ms-STH777QM.mjs.map +1 -0
- package/dist/{nl-YXES36KM.mjs → nl-Y7LECDDR.mjs} +8 -4
- package/dist/nl-Y7LECDDR.mjs.map +1 -0
- package/dist/{no-XRA2UCQD.mjs → no-KEKCEWU6.mjs} +8 -4
- package/dist/no-KEKCEWU6.mjs.map +1 -0
- package/dist/{pl-WH6LJA5G.mjs → pl-7A7OC75O.mjs} +8 -4
- package/dist/pl-7A7OC75O.mjs.map +1 -0
- package/dist/{pt-7GAG57BM.mjs → pt-35HTM7RA.mjs} +8 -4
- package/dist/pt-35HTM7RA.mjs.map +1 -0
- package/dist/{ro-BTDDRB7N.mjs → ro-VAWL5KQA.mjs} +8 -4
- package/dist/ro-VAWL5KQA.mjs.map +1 -0
- package/dist/{sv-7V5C2IT4.mjs → sv-7ZK5EQEB.mjs} +8 -4
- package/dist/sv-7ZK5EQEB.mjs.map +1 -0
- package/dist/test-utils.d.mts +18 -8
- package/dist/test-utils.mjs +36 -14
- package/dist/test-utils.mjs.map +1 -1
- package/dist/{th-LPKYLBX5.mjs → th-UDWZ4X34.mjs} +8 -4
- package/dist/th-UDWZ4X34.mjs.map +1 -0
- package/dist/{tr-DU4RQL4M.mjs → tr-4WMPK3UX.mjs} +8 -4
- package/dist/tr-4WMPK3UX.mjs.map +1 -0
- package/dist/{uk-36UHTDDI.mjs → uk-SSLASQYJ.mjs} +8 -4
- package/dist/uk-SSLASQYJ.mjs.map +1 -0
- package/dist/{vi-GDHOUZDH.mjs → vi-IF42Z5PU.mjs} +8 -4
- package/dist/vi-IF42Z5PU.mjs.map +1 -0
- package/dist/{zh-TYUID4XZ.mjs → zh-HRQTNTAI.mjs} +8 -4
- package/dist/zh-HRQTNTAI.mjs.map +1 -0
- package/package.json +8 -2
- package/src/components/CodeMirrorRenderer.tsx +66 -93
- package/src/components/DetectionProgressWidget.tsx +16 -5
- package/src/components/LiveRegion.tsx +18 -18
- package/src/components/ResizeHandle.tsx +10 -4
- package/src/components/SessionTimer.tsx +2 -2
- package/src/components/Toolbar.tsx +18 -9
- package/src/components/__tests__/SessionTimer.test.tsx +9 -9
- package/src/components/annotation/AnnotateToolbar.tsx +17 -15
- package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +165 -63
- package/src/components/annotation/annotation-entries.css +10 -0
- package/src/components/annotation-popups/JsonLdView.tsx +8 -2
- package/src/components/image-annotation/AnnotationOverlay.tsx +42 -22
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +27 -30
- package/src/components/layout/__tests__/LeftSidebar.test.tsx +12 -33
- package/src/components/layout/__tests__/PageLayout.test.tsx +37 -32
- package/src/components/layout/__tests__/UnifiedHeader.test.tsx +21 -40
- package/src/components/modals/ResourceSearchModal.tsx +2 -2
- package/src/components/modals/SearchModal.tsx +1 -1
- package/src/components/navigation/CollapsibleResourceNavigation.tsx +14 -9
- package/src/components/navigation/NavigationTabs.css +36 -24
- package/src/components/navigation/ObservableLink.tsx +91 -0
- package/src/components/navigation/SimpleNavigation.tsx +20 -16
- package/src/components/navigation/SortableResourceTab.tsx +11 -5
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +51 -26
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +28 -22
- package/src/components/resource/AnnotateView.tsx +64 -138
- package/src/components/resource/AnnotationHistory.tsx +12 -13
- package/src/components/resource/BrowseView.tsx +89 -177
- package/src/components/resource/HistoryEvent.tsx +16 -11
- package/src/components/resource/ResourceViewer.tsx +201 -370
- package/src/components/resource/__tests__/BrowseView.test.tsx +631 -0
- package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +231 -0
- package/src/components/resource/event-formatting.ts +316 -0
- package/src/components/resource/panels/AssessmentEntry.tsx +25 -33
- package/src/components/resource/panels/AssessmentPanel.tsx +137 -31
- package/src/components/resource/panels/CollaborationPanel.tsx +20 -13
- package/src/components/resource/panels/CommentEntry.tsx +38 -32
- package/src/components/resource/panels/CommentsPanel.tsx +153 -31
- package/src/components/resource/panels/DetectSection.css +36 -1
- package/src/components/resource/panels/DetectSection.tsx +38 -10
- package/src/components/resource/panels/HighlightEntry.tsx +25 -33
- package/src/components/resource/panels/HighlightPanel.tsx +100 -25
- package/src/components/resource/panels/ReferenceEntry.tsx +61 -75
- package/src/components/resource/panels/ReferencesPanel.tsx +166 -49
- package/src/components/resource/panels/ResourceInfoPanel.tsx +47 -48
- package/src/components/resource/panels/StatisticsPanel.tsx +9 -19
- package/src/components/resource/panels/TagEntry.tsx +25 -33
- package/src/components/resource/panels/TaggingPanel.tsx +141 -25
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +46 -101
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +566 -0
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +86 -78
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +146 -141
- package/src/components/resource/panels/__tests__/DetectSection.test.tsx +480 -0
- package/src/components/resource/panels/__tests__/HighlightPanel.detectionProgress.test.tsx +362 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +228 -103
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +117 -61
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +586 -0
- package/src/components/settings/SettingsPanel.tsx +15 -12
- package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +1 -46
- package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -9
- package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -3
- package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -9
- package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -3
- package/src/features/admin-users/components/AdminUsersPage.tsx +0 -9
- package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -3
- package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -9
- package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +0 -32
- package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -9
- package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +0 -32
- package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -9
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +51 -54
- package/src/features/resource-compose/components/ResourceComposePage.tsx +3 -13
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +39 -45
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +16 -27
- package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +231 -0
- package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +234 -0
- package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +388 -0
- package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +318 -0
- package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +504 -0
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +145 -91
- package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +322 -0
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +325 -476
- package/src/styles/motivations/motivation-assessment.css +28 -0
- package/src/styles/patterns/panel-helpers.css +26 -0
- package/translations/ar.json +7 -3
- package/translations/bn.json +7 -3
- package/translations/cs.json +7 -3
- package/translations/da.json +7 -3
- package/translations/de.json +7 -3
- package/translations/el.json +7 -3
- package/translations/en.json +7 -3
- package/translations/es.json +7 -3
- package/translations/fa.json +7 -3
- package/translations/fi.json +7 -3
- package/translations/fr.json +7 -3
- package/translations/he.json +7 -3
- package/translations/hi.json +7 -3
- package/translations/id.json +7 -3
- package/translations/it.json +7 -3
- package/translations/ja.json +7 -3
- package/translations/ko.json +7 -3
- package/translations/ms.json +7 -3
- package/translations/nl.json +7 -3
- package/translations/no.json +7 -3
- package/translations/pl.json +7 -3
- package/translations/pt.json +7 -3
- package/translations/ro.json +7 -3
- package/translations/sv.json +7 -3
- package/translations/th.json +7 -3
- package/translations/tr.json +7 -3
- package/translations/uk.json +7 -3
- package/translations/vi.json +7 -3
- package/translations/zh.json +7 -3
- package/dist/PdfAnnotationCanvas.client-ADC4FFSE.mjs.map +0 -1
- package/dist/TranslationManager-Co_5fSxl.d.mts +0 -118
- package/dist/ar-RNNSPLQB.mjs.map +0 -1
- package/dist/bn-S2CDL7EC.mjs.map +0 -1
- package/dist/chunk-35LLVRFK.mjs.map +0 -1
- package/dist/chunk-UDX2Q35T.mjs.map +0 -1
- package/dist/cs-RSV675WU.mjs.map +0 -1
- package/dist/da-CHXNPWJC.mjs.map +0 -1
- package/dist/de-KPEZ53D4.mjs.map +0 -1
- package/dist/el-MW2BME5T.mjs.map +0 -1
- package/dist/es-HQ24NYS3.mjs.map +0 -1
- package/dist/fa-W34LRLHG.mjs.map +0 -1
- package/dist/fi-3U44IGOA.mjs.map +0 -1
- package/dist/fr-N7DKX6NN.mjs.map +0 -1
- package/dist/he-CS4WRXN3.mjs.map +0 -1
- package/dist/hi-GJDY46KA.mjs.map +0 -1
- package/dist/id-WAEZJK2Y.mjs.map +0 -1
- package/dist/it-VDNDMZPU.mjs.map +0 -1
- package/dist/ja-5PEH56J5.mjs.map +0 -1
- package/dist/ko-JYPL3WVA.mjs.map +0 -1
- package/dist/ms-5PZVW76T.mjs.map +0 -1
- package/dist/nl-YXES36KM.mjs.map +0 -1
- package/dist/no-XRA2UCQD.mjs.map +0 -1
- package/dist/pl-WH6LJA5G.mjs.map +0 -1
- package/dist/pt-7GAG57BM.mjs.map +0 -1
- package/dist/ro-BTDDRB7N.mjs.map +0 -1
- package/dist/sv-7V5C2IT4.mjs.map +0 -1
- package/dist/th-LPKYLBX5.mjs.map +0 -1
- package/dist/tr-DU4RQL4M.mjs.map +0 -1
- package/dist/uk-36UHTDDI.mjs.map +0 -1
- package/dist/vi-GDHOUZDH.mjs.map +0 -1
- package/dist/zh-TYUID4XZ.mjs.map +0 -1
- /package/dist/{en-EVMIX24Y.mjs.map → en-KJCJQ4OO.mjs.map} +0 -0
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useState, useRef, useEffect } from 'react';
|
|
3
|
+
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
+
import { useEventBus } from '../../../contexts/EventBusContext';
|
|
6
|
+
import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
|
|
5
7
|
import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
|
|
6
8
|
import { DetectionProgressWidget } from '../../DetectionProgressWidget';
|
|
7
9
|
import { ReferenceEntry } from './ReferenceEntry';
|
|
8
10
|
import type { components, paths, Selector } from '@semiont/api-client';
|
|
9
|
-
import {
|
|
11
|
+
import { getTextPositionSelector, getTargetSelector } from '@semiont/api-client';
|
|
10
12
|
import { PanelHeader } from './PanelHeader';
|
|
11
|
-
import { supportsDetection } from '../../../lib/resource-utils';
|
|
12
13
|
import './ReferencesPanel.css';
|
|
13
14
|
|
|
14
15
|
type Annotation = components['schemas']['Annotation'];
|
|
@@ -47,12 +48,6 @@ interface DetectionLog {
|
|
|
47
48
|
interface Props {
|
|
48
49
|
// Generic panel props
|
|
49
50
|
annotations?: Annotation[];
|
|
50
|
-
onAnnotationClick?: (annotation: Annotation) => void;
|
|
51
|
-
focusedAnnotationId?: string | null;
|
|
52
|
-
hoveredAnnotationId?: string | null;
|
|
53
|
-
onAnnotationHover?: (annotationId: string | null) => void;
|
|
54
|
-
onDetect?: (selectedTypes: string[], includeDescriptiveReferences?: boolean) => void;
|
|
55
|
-
onCreate: (entityType?: string) => void;
|
|
56
51
|
isDetecting: boolean;
|
|
57
52
|
detectionProgress: any; // TODO: type this properly
|
|
58
53
|
annotateMode?: boolean;
|
|
@@ -61,47 +56,48 @@ interface Props {
|
|
|
61
56
|
|
|
62
57
|
// Reference-specific props
|
|
63
58
|
allEntityTypes: string[];
|
|
64
|
-
onCancelDetection: () => void;
|
|
65
|
-
onSearchDocuments?: (referenceId: string, searchTerm: string) => void;
|
|
66
|
-
onGenerateDocument?: (referenceId: string, options: { title: string; prompt?: string }) => void;
|
|
67
|
-
onCreateDocument?: (annotationUri: string, title: string, entityTypes: string[]) => void;
|
|
68
59
|
generatingReferenceId?: string | null;
|
|
69
|
-
mediaType?: string | undefined;
|
|
70
60
|
referencedBy?: ReferencedBy[];
|
|
71
61
|
referencedByLoading?: boolean;
|
|
72
62
|
pendingAnnotation: PendingAnnotation | null;
|
|
63
|
+
scrollToAnnotationId?: string | null;
|
|
64
|
+
onScrollCompleted?: () => void;
|
|
65
|
+
hoveredAnnotationId?: string | null;
|
|
73
66
|
}
|
|
74
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Panel for managing reference annotations with entity type detection
|
|
70
|
+
*
|
|
71
|
+
* @emits detection:start - Start reference detection. Payload: { motivation: 'linking', options: { entityTypes: string[], includeDescriptiveReferences: boolean } }
|
|
72
|
+
* @emits annotation:create - Create new reference annotation. Payload: { motivation: 'linking', selector: Selector | Selector[], body: Body[] }
|
|
73
|
+
* @emits annotation:cancel-pending - Cancel pending reference annotation. Payload: undefined
|
|
74
|
+
* @subscribes annotation:click - Annotation clicked. Payload: { annotationId: string }
|
|
75
|
+
*/
|
|
75
76
|
export function ReferencesPanel({
|
|
76
77
|
annotations = [],
|
|
77
|
-
onAnnotationClick,
|
|
78
|
-
focusedAnnotationId,
|
|
79
|
-
hoveredAnnotationId,
|
|
80
|
-
onAnnotationHover,
|
|
81
|
-
onDetect,
|
|
82
|
-
onCreate,
|
|
83
78
|
isDetecting,
|
|
84
79
|
detectionProgress,
|
|
85
80
|
annotateMode = true,
|
|
86
81
|
Link,
|
|
87
82
|
routes,
|
|
88
83
|
allEntityTypes,
|
|
89
|
-
onCancelDetection,
|
|
90
|
-
onSearchDocuments,
|
|
91
|
-
onGenerateDocument,
|
|
92
|
-
onCreateDocument,
|
|
93
84
|
generatingReferenceId,
|
|
94
|
-
mediaType,
|
|
95
85
|
referencedBy = [],
|
|
96
86
|
referencedByLoading = false,
|
|
97
87
|
pendingAnnotation,
|
|
88
|
+
scrollToAnnotationId,
|
|
89
|
+
onScrollCompleted,
|
|
90
|
+
hoveredAnnotationId,
|
|
98
91
|
}: Props) {
|
|
99
92
|
const t = useTranslations('DetectPanel');
|
|
100
93
|
const tRef = useTranslations('ReferencesPanel');
|
|
94
|
+
const eventBus = useEventBus();
|
|
101
95
|
const [selectedEntityTypes, setSelectedEntityTypes] = useState<string[]>([]);
|
|
102
96
|
const [lastDetectionLog, setLastDetectionLog] = useState<DetectionLog[] | null>(null);
|
|
103
97
|
const [pendingEntityTypes, setPendingEntityTypes] = useState<string[]>([]);
|
|
104
98
|
const [includeDescriptiveReferences, setIncludeDescriptiveReferences] = useState(false);
|
|
99
|
+
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
|
|
100
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
105
101
|
|
|
106
102
|
// Collapsible detection section state - load from localStorage, default expanded
|
|
107
103
|
const [isDetectExpanded, setIsDetectExpanded] = useState(() => {
|
|
@@ -116,17 +112,108 @@ export function ReferencesPanel({
|
|
|
116
112
|
localStorage.setItem('detect-section-expanded-reference', String(isDetectExpanded));
|
|
117
113
|
}, [isDetectExpanded]);
|
|
118
114
|
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
// Direct ref management - replace useAnnotationPanel hook
|
|
116
|
+
const entryRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
|
117
|
+
|
|
118
|
+
// Sort annotations by their position in the resource
|
|
119
|
+
const sortedAnnotations = useMemo(() => {
|
|
120
|
+
return [...annotations].sort((a, b) => {
|
|
121
|
+
const aSelector = getTextPositionSelector(getTargetSelector(a.target));
|
|
122
|
+
const bSelector = getTextPositionSelector(getTargetSelector(b.target));
|
|
123
|
+
if (!aSelector || !bSelector) return 0;
|
|
124
|
+
return aSelector.start - bSelector.start;
|
|
125
|
+
});
|
|
126
|
+
}, [annotations]);
|
|
127
|
+
|
|
128
|
+
// Ref callback for entry components
|
|
129
|
+
const setEntryRef = useCallback((id: string, element: HTMLDivElement | null) => {
|
|
130
|
+
if (element) {
|
|
131
|
+
entryRefs.current.set(id, element);
|
|
132
|
+
} else {
|
|
133
|
+
entryRefs.current.delete(id);
|
|
134
|
+
}
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
137
|
+
// Handle scrollToAnnotationId (click scroll)
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!scrollToAnnotationId) return;
|
|
140
|
+
|
|
141
|
+
const element = entryRefs.current.get(scrollToAnnotationId);
|
|
142
|
+
|
|
143
|
+
if (element && containerRef.current) {
|
|
144
|
+
// Calculate scroll position to center element in container
|
|
145
|
+
const elementTop = element.offsetTop;
|
|
146
|
+
const containerHeight = containerRef.current.clientHeight;
|
|
147
|
+
const elementHeight = element.offsetHeight;
|
|
148
|
+
const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
|
|
149
|
+
|
|
150
|
+
// Scroll to center
|
|
151
|
+
containerRef.current.scrollTo({ top: scrollTo, behavior: 'smooth' });
|
|
152
|
+
|
|
153
|
+
// Add pulse effect
|
|
154
|
+
element.classList.remove('semiont-annotation-pulse');
|
|
155
|
+
void element.offsetWidth; // Force reflow
|
|
156
|
+
element.classList.add('semiont-annotation-pulse');
|
|
157
|
+
|
|
158
|
+
// Notify completion
|
|
159
|
+
if (onScrollCompleted) {
|
|
160
|
+
onScrollCompleted();
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
console.warn('[ReferencesPanel] Element not found for scrollToAnnotationId:', scrollToAnnotationId);
|
|
164
|
+
}
|
|
165
|
+
}, [scrollToAnnotationId]);
|
|
166
|
+
|
|
167
|
+
// Handle hoveredAnnotationId (hover scroll only - pulse is handled by isHovered prop)
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (!hoveredAnnotationId) return;
|
|
170
|
+
|
|
171
|
+
const element = entryRefs.current.get(hoveredAnnotationId);
|
|
172
|
+
|
|
173
|
+
if (!element || !containerRef.current) return;
|
|
174
|
+
|
|
175
|
+
const container = containerRef.current;
|
|
176
|
+
const elementRect = element.getBoundingClientRect();
|
|
177
|
+
const containerRect = container.getBoundingClientRect();
|
|
178
|
+
|
|
179
|
+
// Only scroll if element is not fully visible
|
|
180
|
+
const isVisible =
|
|
181
|
+
elementRect.top >= containerRect.top &&
|
|
182
|
+
elementRect.bottom <= containerRect.bottom;
|
|
183
|
+
|
|
184
|
+
if (!isVisible) {
|
|
185
|
+
const elementTop = element.offsetTop;
|
|
186
|
+
const containerHeight = container.clientHeight;
|
|
187
|
+
const elementHeight = element.offsetHeight;
|
|
188
|
+
const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
|
|
189
|
+
|
|
190
|
+
container.scrollTo({ top: scrollTo, behavior: 'smooth' });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Pulse effect is handled by isHovered prop on ReferenceEntry
|
|
194
|
+
}, [hoveredAnnotationId]);
|
|
121
195
|
|
|
122
|
-
//
|
|
123
|
-
|
|
196
|
+
// Subscribe to click events - update focused state
|
|
197
|
+
// Event handler for annotation clicks (extracted to avoid inline arrow function)
|
|
198
|
+
const handleAnnotationClick = useCallback(({ annotationId }: { annotationId: string }) => {
|
|
199
|
+
setFocusedAnnotationId(annotationId);
|
|
200
|
+
setTimeout(() => setFocusedAnnotationId(null), 3000);
|
|
201
|
+
}, []);
|
|
202
|
+
|
|
203
|
+
useEventSubscriptions({
|
|
204
|
+
'annotation:click': handleAnnotationClick,
|
|
205
|
+
});
|
|
124
206
|
|
|
125
207
|
// Clear log when starting new detection
|
|
126
208
|
const handleDetect = () => {
|
|
127
|
-
if (!onDetect) return;
|
|
128
209
|
setLastDetectionLog(null);
|
|
129
|
-
|
|
210
|
+
eventBus.emit('detection:start', {
|
|
211
|
+
motivation: 'linking',
|
|
212
|
+
options: {
|
|
213
|
+
entityTypes: selectedEntityTypes,
|
|
214
|
+
includeDescriptiveReferences,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
130
217
|
};
|
|
131
218
|
|
|
132
219
|
// Track whether we've already saved the log for the current detection run
|
|
@@ -161,11 +248,32 @@ export function ReferencesPanel({
|
|
|
161
248
|
};
|
|
162
249
|
|
|
163
250
|
const handleCreateReference = () => {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
251
|
+
if (pendingAnnotation) {
|
|
252
|
+
const entityType = pendingEntityTypes.join(',') || undefined;
|
|
253
|
+
eventBus.emit('annotation:create', {
|
|
254
|
+
motivation: 'linking',
|
|
255
|
+
selector: pendingAnnotation.selector,
|
|
256
|
+
body: entityType ? [{ type: 'TextualBody', value: entityType, purpose: 'tagging' }] : [],
|
|
257
|
+
});
|
|
258
|
+
setPendingEntityTypes([]);
|
|
259
|
+
}
|
|
167
260
|
};
|
|
168
261
|
|
|
262
|
+
// Escape key handler for cancelling pending annotation
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
if (!pendingAnnotation) return;
|
|
265
|
+
|
|
266
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
267
|
+
if (e.key === 'Escape') {
|
|
268
|
+
eventBus.emit('annotation:cancel-pending', undefined);
|
|
269
|
+
setPendingEntityTypes([]);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
document.addEventListener('keydown', handleEscape);
|
|
274
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
275
|
+
}, [pendingAnnotation]);
|
|
276
|
+
|
|
169
277
|
return (
|
|
170
278
|
<div className="semiont-panel">
|
|
171
279
|
<PanelHeader annotationType="reference" count={annotations.length} title={tRef('referencesTitle')} />
|
|
@@ -205,20 +313,34 @@ export function ReferencesPanel({
|
|
|
205
313
|
</div>
|
|
206
314
|
)}
|
|
207
315
|
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
316
|
+
<div className="semiont-annotation-prompt__footer">
|
|
317
|
+
<div className="semiont-annotation-prompt__actions">
|
|
318
|
+
<button
|
|
319
|
+
onClick={() => {
|
|
320
|
+
eventBus.emit('annotation:cancel-pending', undefined);
|
|
321
|
+
setPendingEntityTypes([]);
|
|
322
|
+
}}
|
|
323
|
+
className="semiont-button semiont-button--secondary"
|
|
324
|
+
data-type="reference"
|
|
325
|
+
>
|
|
326
|
+
{tRef('cancel')}
|
|
327
|
+
</button>
|
|
328
|
+
<button
|
|
329
|
+
onClick={handleCreateReference}
|
|
330
|
+
className="semiont-button semiont-button--primary"
|
|
331
|
+
data-type="reference"
|
|
332
|
+
>
|
|
333
|
+
🔗 {tRef('createReference')}
|
|
334
|
+
</button>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
215
337
|
</div>
|
|
216
338
|
)}
|
|
217
339
|
|
|
218
340
|
{/* Scrollable content area */}
|
|
219
341
|
<div ref={containerRef} className="semiont-panel__content">
|
|
220
342
|
{/* Detection Section - only in Annotate mode and for text resources */}
|
|
221
|
-
{annotateMode &&
|
|
343
|
+
{annotateMode && (
|
|
222
344
|
<div className="semiont-panel__section">
|
|
223
345
|
<button
|
|
224
346
|
onClick={() => setIsDetectExpanded(!isDetectExpanded)}
|
|
@@ -313,7 +435,6 @@ export function ReferencesPanel({
|
|
|
313
435
|
{detectionProgress && (
|
|
314
436
|
<DetectionProgressWidget
|
|
315
437
|
progress={detectionProgress}
|
|
316
|
-
onCancel={onCancelDetection}
|
|
317
438
|
annotationType="reference"
|
|
318
439
|
/>
|
|
319
440
|
)}
|
|
@@ -364,15 +485,11 @@ export function ReferencesPanel({
|
|
|
364
485
|
key={reference.id}
|
|
365
486
|
reference={reference}
|
|
366
487
|
isFocused={reference.id === focusedAnnotationId}
|
|
367
|
-
|
|
488
|
+
isHovered={reference.id === hoveredAnnotationId}
|
|
368
489
|
routes={routes}
|
|
369
|
-
onReferenceRef={handleAnnotationRef}
|
|
370
490
|
annotateMode={annotateMode}
|
|
371
491
|
isGenerating={reference.id === generatingReferenceId}
|
|
372
|
-
{
|
|
373
|
-
{...(onGenerateDocument && { onGenerateDocument })}
|
|
374
|
-
{...(onCreateDocument && { onCreateDocument })}
|
|
375
|
-
{...(onSearchDocuments && { onSearchDocuments })}
|
|
492
|
+
ref={(el) => setEntryRef(reference.id, el)}
|
|
376
493
|
/>
|
|
377
494
|
))
|
|
378
495
|
)}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
4
|
+
import { useEventBus } from '../../../contexts/EventBusContext';
|
|
4
5
|
import { formatLocaleDisplay } from '@semiont/api-client';
|
|
5
6
|
import './ResourceInfoPanel.css';
|
|
6
7
|
|
|
@@ -10,22 +11,24 @@ interface Props {
|
|
|
10
11
|
primaryMediaType?: string | undefined;
|
|
11
12
|
primaryByteSize?: number | undefined;
|
|
12
13
|
isArchived?: boolean;
|
|
13
|
-
onArchive?: () => void;
|
|
14
|
-
onUnarchive?: () => void;
|
|
15
|
-
onClone?: () => void;
|
|
16
14
|
}
|
|
17
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Panel for displaying resource metadata and management actions
|
|
18
|
+
*
|
|
19
|
+
* @emits resource:clone - Clone this resource. Payload: undefined
|
|
20
|
+
* @emits resource:unarchive - Unarchive this resource. Payload: undefined
|
|
21
|
+
* @emits resource:archive - Archive this resource. Payload: undefined
|
|
22
|
+
*/
|
|
18
23
|
export function ResourceInfoPanel({
|
|
19
24
|
documentEntityTypes,
|
|
20
25
|
documentLocale,
|
|
21
26
|
primaryMediaType,
|
|
22
27
|
primaryByteSize,
|
|
23
28
|
isArchived = false,
|
|
24
|
-
onArchive,
|
|
25
|
-
onUnarchive,
|
|
26
|
-
onClone
|
|
27
29
|
}: Props) {
|
|
28
30
|
const t = useTranslations('ResourceInfoPanel');
|
|
31
|
+
const eventBus = useEventBus();
|
|
29
32
|
|
|
30
33
|
return (
|
|
31
34
|
<div className="semiont-resource-info-panel">
|
|
@@ -92,50 +95,46 @@ export function ResourceInfoPanel({
|
|
|
92
95
|
)}
|
|
93
96
|
|
|
94
97
|
{/* Clone Action */}
|
|
95
|
-
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
</div>
|
|
107
|
-
)}
|
|
98
|
+
<div className="semiont-resource-info-panel__action-section">
|
|
99
|
+
<button
|
|
100
|
+
onClick={() => eventBus.emit('resource:clone', undefined)}
|
|
101
|
+
className="semiont-resource-button semiont-resource-button--secondary"
|
|
102
|
+
>
|
|
103
|
+
🔗 {t('clone')}
|
|
104
|
+
</button>
|
|
105
|
+
<p className="semiont-resource-info-panel__description">
|
|
106
|
+
{t('cloneDescription')}
|
|
107
|
+
</p>
|
|
108
|
+
</div>
|
|
108
109
|
|
|
109
110
|
{/* Archive/Unarchive Actions */}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
</div>
|
|
138
|
-
)}
|
|
111
|
+
<div className="semiont-resource-info-panel__action-section">
|
|
112
|
+
{isArchived ? (
|
|
113
|
+
<>
|
|
114
|
+
<button
|
|
115
|
+
onClick={() => eventBus.emit('resource:unarchive', undefined)}
|
|
116
|
+
className="semiont-resource-button semiont-resource-button--secondary"
|
|
117
|
+
>
|
|
118
|
+
📤 {t('unarchive')}
|
|
119
|
+
</button>
|
|
120
|
+
<p className="semiont-resource-info-panel__description">
|
|
121
|
+
{t('unarchiveDescription')}
|
|
122
|
+
</p>
|
|
123
|
+
</>
|
|
124
|
+
) : (
|
|
125
|
+
<>
|
|
126
|
+
<button
|
|
127
|
+
onClick={() => eventBus.emit('resource:archive', undefined)}
|
|
128
|
+
className="semiont-resource-button semiont-resource-button--archive"
|
|
129
|
+
>
|
|
130
|
+
📦 {t('archive')}
|
|
131
|
+
</button>
|
|
132
|
+
<p className="semiont-resource-info-panel__description">
|
|
133
|
+
{t('archiveDescription')}
|
|
134
|
+
</p>
|
|
135
|
+
</>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
139
138
|
</div>
|
|
140
139
|
);
|
|
141
140
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useMemo } from 'react';
|
|
4
3
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
4
|
import type { components } from '@semiont/api-client';
|
|
6
5
|
import { isBodyResolved } from '@semiont/api-client';
|
|
@@ -27,28 +26,19 @@ export function StatisticsPanel({
|
|
|
27
26
|
const t = useTranslations('StatisticsPanel');
|
|
28
27
|
|
|
29
28
|
// Count stub vs resolved references
|
|
30
|
-
const stubCount =
|
|
31
|
-
|
|
32
|
-
[references]
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
const resolvedCount = useMemo(
|
|
36
|
-
() => references.filter((r) => isBodyResolved(r.body)).length,
|
|
37
|
-
[references]
|
|
38
|
-
);
|
|
29
|
+
const stubCount = references.filter((r) => !isBodyResolved(r.body)).length;
|
|
30
|
+
const resolvedCount = references.filter((r) => isBodyResolved(r.body)).length;
|
|
39
31
|
|
|
40
32
|
// Count entity types from references (at annotation level)
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
entityTypeCounts.set(type, (entityTypeCounts.get(type) || 0) + 1);
|
|
47
|
-
});
|
|
33
|
+
const entityTypeCounts = new Map<string, number>();
|
|
34
|
+
references.forEach((ref) => {
|
|
35
|
+
const entityTypes = getEntityTypes(ref);
|
|
36
|
+
entityTypes.forEach((type: string) => {
|
|
37
|
+
entityTypeCounts.set(type, (entityTypeCounts.get(type) || 0) + 1);
|
|
48
38
|
});
|
|
39
|
+
});
|
|
49
40
|
|
|
50
|
-
|
|
51
|
-
}, [references]);
|
|
41
|
+
const entityTypesList = Array.from(entityTypeCounts.entries()).sort((a, b) => b[1] - a[1]); // Sort by count descending
|
|
52
42
|
|
|
53
43
|
return (
|
|
54
44
|
<div className="semiont-statistics-panel">
|
|
@@ -1,44 +1,30 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
4
|
import type { components } from '@semiont/api-client';
|
|
5
5
|
import { getAnnotationExactText } from '@semiont/api-client';
|
|
6
6
|
import { getTagCategory, getTagSchemaId } from '@semiont/ontology';
|
|
7
7
|
import { getTagSchema } from '../../../lib/tag-schemas';
|
|
8
|
+
import { useEventBus } from '../../../contexts/EventBusContext';
|
|
8
9
|
|
|
9
10
|
type Annotation = components['schemas']['Annotation'];
|
|
10
11
|
|
|
11
12
|
interface TagEntryProps {
|
|
12
13
|
tag: Annotation;
|
|
13
14
|
isFocused: boolean;
|
|
14
|
-
|
|
15
|
-
onTagRef: (tagId: string, el: HTMLElement | null) => void;
|
|
16
|
-
onTagHover?: (tagId: string | null) => void;
|
|
15
|
+
isHovered?: boolean;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
onTagRef(tag.id, tagRef.current);
|
|
31
|
-
return () => {
|
|
32
|
-
onTagRef(tag.id, null);
|
|
33
|
-
};
|
|
34
|
-
}, [tag.id, onTagRef]);
|
|
35
|
-
|
|
36
|
-
// Scroll to tag when focused
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
if (isFocused && tagRef.current) {
|
|
39
|
-
tagRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
40
|
-
}
|
|
41
|
-
}, [isFocused]);
|
|
18
|
+
export const TagEntry = forwardRef<HTMLDivElement, TagEntryProps>(
|
|
19
|
+
function TagEntry(
|
|
20
|
+
{
|
|
21
|
+
tag,
|
|
22
|
+
isFocused,
|
|
23
|
+
isHovered = false,
|
|
24
|
+
},
|
|
25
|
+
ref
|
|
26
|
+
) {
|
|
27
|
+
const eventBus = useEventBus();
|
|
42
28
|
|
|
43
29
|
const selectedText = getAnnotationExactText(tag);
|
|
44
30
|
const category = getTagCategory(tag);
|
|
@@ -47,11 +33,17 @@ export function TagEntry({
|
|
|
47
33
|
|
|
48
34
|
return (
|
|
49
35
|
<div
|
|
50
|
-
ref={
|
|
51
|
-
onClick={
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
36
|
+
ref={ref}
|
|
37
|
+
onClick={() => {
|
|
38
|
+
eventBus.emit('annotation:click', { annotationId: tag.id, motivation: tag.motivation });
|
|
39
|
+
}}
|
|
40
|
+
onMouseEnter={() => {
|
|
41
|
+
eventBus.emit('annotation:hover', { annotationId: tag.id });
|
|
42
|
+
}}
|
|
43
|
+
onMouseLeave={() => {
|
|
44
|
+
eventBus.emit('annotation:hover', { annotationId: null });
|
|
45
|
+
}}
|
|
46
|
+
className={`semiont-annotation-entry${isHovered ? ' semiont-annotation-pulse' : ''}`}
|
|
55
47
|
data-type="tag"
|
|
56
48
|
data-focused={isFocused ? 'true' : 'false'}
|
|
57
49
|
>
|
|
@@ -73,4 +65,4 @@ export function TagEntry({
|
|
|
73
65
|
</div>
|
|
74
66
|
</div>
|
|
75
67
|
);
|
|
76
|
-
}
|
|
68
|
+
});
|