@semiont/react-ui 0.2.33-build.79 → 0.2.33-build.81
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-CJjL_cCf.d.mts +462 -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-EMHEHPCJ.mjs → ar-4ZEORRW2.mjs} +7 -4
- package/dist/ar-4ZEORRW2.mjs.map +1 -0
- package/dist/{bn-OVCI4F6X.mjs → bn-SEDE5BQJ.mjs} +7 -4
- package/dist/bn-SEDE5BQJ.mjs.map +1 -0
- package/dist/{chunk-LIHZTECW.mjs → chunk-D7NBW4RV.mjs} +7 -4
- package/dist/chunk-D7NBW4RV.mjs.map +1 -0
- package/dist/{chunk-JZIO2A3B.mjs → chunk-QB52Q7EQ.mjs} +206 -146
- package/dist/chunk-QB52Q7EQ.mjs.map +1 -0
- package/dist/{cs-FAN66Q2F.mjs → cs-7W4WF5WD.mjs} +7 -4
- package/dist/cs-7W4WF5WD.mjs.map +1 -0
- package/dist/{da-YBBIHI2O.mjs → da-75XGBCBK.mjs} +7 -4
- package/dist/da-75XGBCBK.mjs.map +1 -0
- package/dist/{de-MAYU33LB.mjs → de-ODJVFLHM.mjs} +7 -4
- package/dist/de-ODJVFLHM.mjs.map +1 -0
- package/dist/{el-MKGSWN4O.mjs → el-C4PM4WB3.mjs} +7 -4
- package/dist/el-C4PM4WB3.mjs.map +1 -0
- package/dist/{en-DDLIXJCU.mjs → en-KJCJQ4OO.mjs} +2 -2
- package/dist/{es-52LHUWJD.mjs → es-WD33R7QL.mjs} +7 -4
- package/dist/es-WD33R7QL.mjs.map +1 -0
- package/dist/{fa-FJICRANB.mjs → fa-2BP6V56P.mjs} +7 -4
- package/dist/fa-2BP6V56P.mjs.map +1 -0
- package/dist/{fi-O455XFCR.mjs → fi-USRRW24J.mjs} +7 -4
- package/dist/fi-USRRW24J.mjs.map +1 -0
- package/dist/{fr-TXIXHOOE.mjs → fr-EC5S6WVF.mjs} +7 -4
- package/dist/fr-EC5S6WVF.mjs.map +1 -0
- package/dist/{he-JBSOX5IN.mjs → he-7TBVIKAA.mjs} +7 -4
- package/dist/he-7TBVIKAA.mjs.map +1 -0
- package/dist/{hi-KGHI3XVT.mjs → hi-FO4VIZLA.mjs} +7 -4
- package/dist/hi-FO4VIZLA.mjs.map +1 -0
- package/dist/{id-5OCPPZLO.mjs → id-7U7GGVWY.mjs} +7 -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 +715 -574
- package/dist/index.mjs +3898 -3575
- package/dist/index.mjs.map +1 -1
- package/dist/{it-PNBBZSM2.mjs → it-Y4OPL6I2.mjs} +7 -4
- package/dist/it-Y4OPL6I2.mjs.map +1 -0
- package/dist/{ja-LDD7R3TJ.mjs → ja-PK7SQL55.mjs} +7 -4
- package/dist/ja-PK7SQL55.mjs.map +1 -0
- package/dist/{ko-F47ZDEY3.mjs → ko-L25PXMYD.mjs} +7 -4
- package/dist/ko-L25PXMYD.mjs.map +1 -0
- package/dist/{ms-Z7LMXJWL.mjs → ms-STH777QM.mjs} +7 -4
- package/dist/ms-STH777QM.mjs.map +1 -0
- package/dist/{nl-6SJFBPJ3.mjs → nl-Y7LECDDR.mjs} +7 -4
- package/dist/nl-Y7LECDDR.mjs.map +1 -0
- package/dist/{no-YXPBPSGF.mjs → no-KEKCEWU6.mjs} +7 -4
- package/dist/no-KEKCEWU6.mjs.map +1 -0
- package/dist/{pl-P4AZ2QME.mjs → pl-7A7OC75O.mjs} +7 -4
- package/dist/pl-7A7OC75O.mjs.map +1 -0
- package/dist/{pt-LHWUS6U6.mjs → pt-35HTM7RA.mjs} +7 -4
- package/dist/pt-35HTM7RA.mjs.map +1 -0
- package/dist/{ro-EA5J2ZON.mjs → ro-VAWL5KQA.mjs} +7 -4
- package/dist/ro-VAWL5KQA.mjs.map +1 -0
- package/dist/{sv-DATBS3UQ.mjs → sv-7ZK5EQEB.mjs} +7 -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-WTFJRWPT.mjs → th-UDWZ4X34.mjs} +7 -4
- package/dist/th-UDWZ4X34.mjs.map +1 -0
- package/dist/{tr-IKO3RXOX.mjs → tr-4WMPK3UX.mjs} +7 -4
- package/dist/tr-4WMPK3UX.mjs.map +1 -0
- package/dist/{uk-CF6CTTRK.mjs → uk-SSLASQYJ.mjs} +7 -4
- package/dist/uk-SSLASQYJ.mjs.map +1 -0
- package/dist/{vi-AJLTXPZQ.mjs → vi-IF42Z5PU.mjs} +7 -4
- package/dist/vi-IF42Z5PU.mjs.map +1 -0
- package/dist/{zh-U3ORHHYH.mjs → zh-HRQTNTAI.mjs} +7 -4
- package/dist/zh-HRQTNTAI.mjs.map +1 -0
- package/package.json +3 -1
- package/src/components/CodeMirrorRenderer.tsx +66 -93
- package/src/components/DetectionProgressWidget.tsx +16 -5
- package/src/components/ResizeHandle.tsx +10 -4
- package/src/components/SessionExpiryBanner.tsx +2 -3
- package/src/components/SessionTimer.tsx +3 -3
- package/src/components/Toolbar.tsx +18 -9
- package/src/components/__tests__/SessionTimer.test.tsx +33 -33
- 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 -134
- package/src/components/resource/BrowseView.tsx +86 -166
- package/src/components/resource/HistoryEvent.tsx +13 -7
- package/src/components/resource/ResourceViewer.tsx +122 -264
- 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/panels/AssessmentEntry.tsx +25 -33
- package/src/components/resource/panels/AssessmentPanel.tsx +106 -28
- package/src/components/resource/panels/CommentEntry.tsx +38 -32
- package/src/components/resource/panels/CommentsPanel.tsx +121 -28
- package/src/components/resource/panels/DetectSection.css +36 -1
- package/src/components/resource/panels/DetectSection.tsx +49 -15
- 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 +134 -42
- package/src/components/resource/panels/ResourceInfoPanel.tsx +47 -48
- package/src/components/resource/panels/TagEntry.tsx +25 -33
- package/src/components/resource/panels/TaggingPanel.tsx +118 -30
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +30 -92
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +129 -110
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +86 -78
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +144 -149
- 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 +226 -111
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +117 -61
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +128 -106
- 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 +9 -13
- package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +234 -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 +503 -0
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +139 -93
- package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +322 -0
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +341 -524
- package/translations/ar.json +6 -3
- package/translations/bn.json +6 -3
- package/translations/cs.json +6 -3
- package/translations/da.json +6 -3
- package/translations/de.json +6 -3
- package/translations/el.json +6 -3
- package/translations/en.json +6 -3
- package/translations/es.json +6 -3
- package/translations/fa.json +6 -3
- package/translations/fi.json +6 -3
- package/translations/fr.json +6 -3
- package/translations/he.json +6 -3
- package/translations/hi.json +6 -3
- package/translations/id.json +6 -3
- package/translations/it.json +6 -3
- package/translations/ja.json +6 -3
- package/translations/ko.json +6 -3
- package/translations/ms.json +6 -3
- package/translations/nl.json +6 -3
- package/translations/no.json +6 -3
- package/translations/pl.json +6 -3
- package/translations/pt.json +6 -3
- package/translations/ro.json +6 -3
- package/translations/sv.json +6 -3
- package/translations/th.json +6 -3
- package/translations/tr.json +6 -3
- package/translations/uk.json +6 -3
- package/translations/vi.json +6 -3
- package/translations/zh.json +6 -3
- package/dist/PdfAnnotationCanvas.client-ADC4FFSE.mjs.map +0 -1
- package/dist/TranslationManager-Co_5fSxl.d.mts +0 -118
- package/dist/ar-EMHEHPCJ.mjs.map +0 -1
- package/dist/bn-OVCI4F6X.mjs.map +0 -1
- package/dist/chunk-JZIO2A3B.mjs.map +0 -1
- package/dist/chunk-LIHZTECW.mjs.map +0 -1
- package/dist/cs-FAN66Q2F.mjs.map +0 -1
- package/dist/da-YBBIHI2O.mjs.map +0 -1
- package/dist/de-MAYU33LB.mjs.map +0 -1
- package/dist/el-MKGSWN4O.mjs.map +0 -1
- package/dist/es-52LHUWJD.mjs.map +0 -1
- package/dist/fa-FJICRANB.mjs.map +0 -1
- package/dist/fi-O455XFCR.mjs.map +0 -1
- package/dist/fr-TXIXHOOE.mjs.map +0 -1
- package/dist/he-JBSOX5IN.mjs.map +0 -1
- package/dist/hi-KGHI3XVT.mjs.map +0 -1
- package/dist/id-5OCPPZLO.mjs.map +0 -1
- package/dist/it-PNBBZSM2.mjs.map +0 -1
- package/dist/ja-LDD7R3TJ.mjs.map +0 -1
- package/dist/ko-F47ZDEY3.mjs.map +0 -1
- package/dist/ms-Z7LMXJWL.mjs.map +0 -1
- package/dist/nl-6SJFBPJ3.mjs.map +0 -1
- package/dist/no-YXPBPSGF.mjs.map +0 -1
- package/dist/pl-P4AZ2QME.mjs.map +0 -1
- package/dist/pt-LHWUS6U6.mjs.map +0 -1
- package/dist/ro-EA5J2ZON.mjs.map +0 -1
- package/dist/sv-DATBS3UQ.mjs.map +0 -1
- package/dist/th-WTFJRWPT.mjs.map +0 -1
- package/dist/tr-IKO3RXOX.mjs.map +0 -1
- package/dist/uk-CF6CTTRK.mjs.map +0 -1
- package/dist/vi-AJLTXPZQ.mjs.map +0 -1
- package/dist/zh-U3ORHHYH.mjs.map +0 -1
- /package/dist/{en-DDLIXJCU.mjs.map → en-KJCJQ4OO.mjs.map} +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
3
|
+
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
-
import {
|
|
5
|
+
import { useEventBus } from '../../../contexts/EventBusContext';
|
|
6
|
+
import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
|
|
6
7
|
import type { components, Selector } from '@semiont/api-client';
|
|
8
|
+
import { getTextPositionSelector, getTargetSelector } from '@semiont/api-client';
|
|
7
9
|
import { CommentEntry } from './CommentEntry';
|
|
8
|
-
import { useAnnotationPanel } from '../../../hooks/useAnnotationPanel';
|
|
9
10
|
import { DetectSection } from './DetectSection';
|
|
10
11
|
import { PanelHeader } from './PanelHeader';
|
|
11
12
|
import './CommentsPanel.css';
|
|
@@ -38,45 +39,139 @@ function getSelectorDisplayText(selector: Selector | Selector[]): string | null
|
|
|
38
39
|
|
|
39
40
|
interface CommentsPanelProps {
|
|
40
41
|
annotations: Annotation[];
|
|
41
|
-
onAnnotationClick: (annotation: Annotation) => void;
|
|
42
|
-
onCreate: (commentText: string) => void;
|
|
43
|
-
focusedAnnotationId: string | null;
|
|
44
|
-
hoveredAnnotationId?: string | null;
|
|
45
|
-
onAnnotationHover?: (annotationId: string | null) => void;
|
|
46
42
|
pendingAnnotation: PendingAnnotation | null;
|
|
47
43
|
annotateMode?: boolean;
|
|
48
|
-
onDetect?: (instructions?: string, tone?: string) => void | Promise<void>;
|
|
49
44
|
isDetecting?: boolean;
|
|
50
45
|
detectionProgress?: {
|
|
51
46
|
status: string;
|
|
52
47
|
percentage?: number;
|
|
53
48
|
message?: string;
|
|
54
49
|
} | null;
|
|
50
|
+
scrollToAnnotationId?: string | null;
|
|
51
|
+
onScrollCompleted?: () => void;
|
|
52
|
+
hoveredAnnotationId?: string | null;
|
|
55
53
|
}
|
|
56
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Panel for managing comment annotations with text input
|
|
57
|
+
*
|
|
58
|
+
* @emits annotation:create - Create new comment annotation. Payload: { motivation: 'commenting', selector: Selector | Selector[], body: Body[] }
|
|
59
|
+
* @emits annotation:cancel-pending - Cancel pending comment annotation. Payload: undefined
|
|
60
|
+
* @subscribes annotation:click - Annotation clicked. Payload: { annotationId: string }
|
|
61
|
+
*/
|
|
57
62
|
export function CommentsPanel({
|
|
58
63
|
annotations,
|
|
59
|
-
onAnnotationClick,
|
|
60
|
-
onCreate,
|
|
61
|
-
focusedAnnotationId,
|
|
62
|
-
hoveredAnnotationId,
|
|
63
|
-
onAnnotationHover,
|
|
64
64
|
pendingAnnotation,
|
|
65
65
|
annotateMode = true,
|
|
66
|
-
onDetect,
|
|
67
66
|
isDetecting = false,
|
|
68
67
|
detectionProgress,
|
|
68
|
+
scrollToAnnotationId,
|
|
69
|
+
onScrollCompleted,
|
|
70
|
+
hoveredAnnotationId,
|
|
69
71
|
}: CommentsPanelProps) {
|
|
70
72
|
const t = useTranslations('CommentsPanel');
|
|
71
|
-
const eventBus =
|
|
73
|
+
const eventBus = useEventBus();
|
|
72
74
|
const [newCommentText, setNewCommentText] = useState('');
|
|
75
|
+
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
|
|
76
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
77
|
+
|
|
78
|
+
// Direct ref management - replace useAnnotationPanel hook
|
|
79
|
+
const entryRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
|
80
|
+
|
|
81
|
+
// Sort annotations by their position in the resource
|
|
82
|
+
const sortedAnnotations = useMemo(() => {
|
|
83
|
+
return [...annotations].sort((a, b) => {
|
|
84
|
+
const aSelector = getTextPositionSelector(getTargetSelector(a.target));
|
|
85
|
+
const bSelector = getTextPositionSelector(getTargetSelector(b.target));
|
|
86
|
+
if (!aSelector || !bSelector) return 0;
|
|
87
|
+
return aSelector.start - bSelector.start;
|
|
88
|
+
});
|
|
89
|
+
}, [annotations]);
|
|
90
|
+
|
|
91
|
+
// Ref callback for entry components
|
|
92
|
+
const setEntryRef = useCallback((id: string, element: HTMLDivElement | null) => {
|
|
93
|
+
if (element) {
|
|
94
|
+
entryRefs.current.set(id, element);
|
|
95
|
+
} else {
|
|
96
|
+
entryRefs.current.delete(id);
|
|
97
|
+
}
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
// Handle scrollToAnnotationId (click scroll)
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!scrollToAnnotationId) return;
|
|
103
|
+
|
|
104
|
+
const element = entryRefs.current.get(scrollToAnnotationId);
|
|
105
|
+
|
|
106
|
+
if (element && containerRef.current) {
|
|
107
|
+
// Calculate scroll position to center element in container
|
|
108
|
+
const elementTop = element.offsetTop;
|
|
109
|
+
const containerHeight = containerRef.current.clientHeight;
|
|
110
|
+
const elementHeight = element.offsetHeight;
|
|
111
|
+
const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
|
|
112
|
+
|
|
113
|
+
// Scroll to center
|
|
114
|
+
containerRef.current.scrollTo({ top: scrollTo, behavior: 'smooth' });
|
|
115
|
+
|
|
116
|
+
// Add pulse effect
|
|
117
|
+
element.classList.remove('semiont-annotation-pulse');
|
|
118
|
+
void element.offsetWidth; // Force reflow
|
|
119
|
+
element.classList.add('semiont-annotation-pulse');
|
|
120
|
+
|
|
121
|
+
// Notify completion
|
|
122
|
+
if (onScrollCompleted) {
|
|
123
|
+
onScrollCompleted();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}, [scrollToAnnotationId]);
|
|
127
|
+
|
|
128
|
+
// Handle hoveredAnnotationId (hover scroll only - pulse is handled by isHovered prop)
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (!hoveredAnnotationId) return;
|
|
131
|
+
|
|
132
|
+
const element = entryRefs.current.get(hoveredAnnotationId);
|
|
133
|
+
|
|
134
|
+
if (!element || !containerRef.current) return;
|
|
135
|
+
|
|
136
|
+
const container = containerRef.current;
|
|
137
|
+
const elementRect = element.getBoundingClientRect();
|
|
138
|
+
const containerRect = container.getBoundingClientRect();
|
|
139
|
+
|
|
140
|
+
// Only scroll if element is not fully visible
|
|
141
|
+
const isVisible =
|
|
142
|
+
elementRect.top >= containerRect.top &&
|
|
143
|
+
elementRect.bottom <= containerRect.bottom;
|
|
144
|
+
|
|
145
|
+
if (!isVisible) {
|
|
146
|
+
const elementTop = element.offsetTop;
|
|
147
|
+
const containerHeight = container.clientHeight;
|
|
148
|
+
const elementHeight = element.offsetHeight;
|
|
149
|
+
const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
|
|
150
|
+
|
|
151
|
+
container.scrollTo({ top: scrollTo, behavior: 'smooth' });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Pulse effect is handled by isHovered prop on CommentEntry
|
|
155
|
+
}, [hoveredAnnotationId]);
|
|
156
|
+
|
|
157
|
+
// Subscribe to click events - update focused state
|
|
158
|
+
// Event handler for annotation clicks (extracted to avoid inline arrow function)
|
|
159
|
+
const handleAnnotationClick = useCallback(({ annotationId }: { annotationId: string }) => {
|
|
160
|
+
setFocusedAnnotationId(annotationId);
|
|
161
|
+
setTimeout(() => setFocusedAnnotationId(null), 3000);
|
|
162
|
+
}, []);
|
|
73
163
|
|
|
74
|
-
|
|
75
|
-
|
|
164
|
+
useEventSubscriptions({
|
|
165
|
+
'annotation:click': handleAnnotationClick,
|
|
166
|
+
});
|
|
76
167
|
|
|
77
168
|
const handleSaveNewComment = () => {
|
|
78
|
-
if (newCommentText.trim()) {
|
|
79
|
-
|
|
169
|
+
if (newCommentText.trim() && pendingAnnotation) {
|
|
170
|
+
eventBus.emit('annotation:create', {
|
|
171
|
+
motivation: 'commenting',
|
|
172
|
+
selector: pendingAnnotation.selector,
|
|
173
|
+
body: [{ type: 'TextualBody', value: newCommentText, purpose: 'commenting' }],
|
|
174
|
+
});
|
|
80
175
|
setNewCommentText('');
|
|
81
176
|
}
|
|
82
177
|
};
|
|
@@ -87,14 +182,14 @@ export function CommentsPanel({
|
|
|
87
182
|
|
|
88
183
|
const handleEscape = (e: KeyboardEvent) => {
|
|
89
184
|
if (e.key === 'Escape') {
|
|
90
|
-
eventBus.emit('
|
|
185
|
+
eventBus.emit('annotation:cancel-pending', undefined);
|
|
91
186
|
setNewCommentText('');
|
|
92
187
|
}
|
|
93
188
|
};
|
|
94
189
|
|
|
95
190
|
document.addEventListener('keydown', handleEscape);
|
|
96
191
|
return () => document.removeEventListener('keydown', handleEscape);
|
|
97
|
-
}, [pendingAnnotation
|
|
192
|
+
}, [pendingAnnotation]);
|
|
98
193
|
|
|
99
194
|
return (
|
|
100
195
|
<div className="semiont-panel">
|
|
@@ -129,7 +224,7 @@ export function CommentsPanel({
|
|
|
129
224
|
<div className="semiont-annotation-prompt__actions">
|
|
130
225
|
<button
|
|
131
226
|
onClick={() => {
|
|
132
|
-
eventBus.emit('
|
|
227
|
+
eventBus.emit('annotation:cancel-pending', undefined);
|
|
133
228
|
setNewCommentText('');
|
|
134
229
|
}}
|
|
135
230
|
className="semiont-button semiont-button--secondary"
|
|
@@ -153,12 +248,11 @@ export function CommentsPanel({
|
|
|
153
248
|
{/* Scrollable content area */}
|
|
154
249
|
<div ref={containerRef} className="semiont-panel__content">
|
|
155
250
|
{/* Detection Section - only in Annotate mode and for text resources */}
|
|
156
|
-
{annotateMode &&
|
|
251
|
+
{annotateMode && (
|
|
157
252
|
<DetectSection
|
|
158
253
|
annotationType="comment"
|
|
159
254
|
isDetecting={isDetecting}
|
|
160
255
|
detectionProgress={detectionProgress}
|
|
161
|
-
onDetect={onDetect}
|
|
162
256
|
/>
|
|
163
257
|
)}
|
|
164
258
|
|
|
@@ -174,10 +268,9 @@ export function CommentsPanel({
|
|
|
174
268
|
key={comment.id}
|
|
175
269
|
comment={comment}
|
|
176
270
|
isFocused={comment.id === focusedAnnotationId}
|
|
177
|
-
|
|
178
|
-
onCommentRef={handleAnnotationRef}
|
|
179
|
-
{...(onAnnotationHover && { onCommentHover: onAnnotationHover })}
|
|
271
|
+
isHovered={comment.id === hoveredAnnotationId}
|
|
180
272
|
annotateMode={annotateMode}
|
|
273
|
+
ref={(el) => setEntryRef(comment.id, el)}
|
|
181
274
|
/>
|
|
182
275
|
))
|
|
183
276
|
)}
|
|
@@ -320,6 +320,7 @@
|
|
|
320
320
|
display: flex;
|
|
321
321
|
flex-direction: column;
|
|
322
322
|
gap: 0.5rem;
|
|
323
|
+
position: relative;
|
|
323
324
|
}
|
|
324
325
|
|
|
325
326
|
.semiont-detection-progress__message {
|
|
@@ -329,13 +330,47 @@
|
|
|
329
330
|
font-size: 0.875rem;
|
|
330
331
|
color: var(--semiont-color-gray-900);
|
|
331
332
|
font-weight: 500;
|
|
332
|
-
padding: 0.75rem 1rem;
|
|
333
|
+
padding: 0.75rem 2.5rem 0.75rem 1rem; /* Extra right padding for close button */
|
|
333
334
|
border-radius: 0.5rem;
|
|
334
335
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(37, 99, 235, 0.15));
|
|
335
336
|
border: 2px solid rgba(59, 130, 246, 0.3);
|
|
336
337
|
animation: semiont-detection-pulse 2s ease-in-out infinite;
|
|
337
338
|
}
|
|
338
339
|
|
|
340
|
+
.semiont-detection-progress__close {
|
|
341
|
+
position: absolute;
|
|
342
|
+
top: 0.5rem;
|
|
343
|
+
right: 0.5rem;
|
|
344
|
+
width: 1.5rem;
|
|
345
|
+
height: 1.5rem;
|
|
346
|
+
border: none;
|
|
347
|
+
background-color: rgba(0, 0, 0, 0.1);
|
|
348
|
+
color: var(--semiont-color-gray-700);
|
|
349
|
+
font-size: 1.25rem;
|
|
350
|
+
line-height: 1;
|
|
351
|
+
border-radius: 0.25rem;
|
|
352
|
+
cursor: pointer;
|
|
353
|
+
display: flex;
|
|
354
|
+
align-items: center;
|
|
355
|
+
justify-content: center;
|
|
356
|
+
transition: all 0.2s ease;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.semiont-detection-progress__close:hover {
|
|
360
|
+
background-color: rgba(0, 0, 0, 0.2);
|
|
361
|
+
color: var(--semiont-color-gray-900);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
[data-theme="dark"] .semiont-detection-progress__close {
|
|
365
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
366
|
+
color: var(--semiont-color-gray-300);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
[data-theme="dark"] .semiont-detection-progress__close:hover {
|
|
370
|
+
background-color: rgba(255, 255, 255, 0.2);
|
|
371
|
+
color: var(--semiont-color-gray-100);
|
|
372
|
+
}
|
|
373
|
+
|
|
339
374
|
[data-theme="dark"] .semiont-detection-progress__message {
|
|
340
375
|
color: var(--semiont-color-gray-100);
|
|
341
376
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.25), rgba(37, 99, 235, 0.25));
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
+
import { useEventBus } from '../../../contexts/EventBusContext';
|
|
6
|
+
import type { Motivation } from '@semiont/api-client';
|
|
5
7
|
import './DetectSection.css';
|
|
6
8
|
|
|
7
9
|
interface DetectSectionProps {
|
|
@@ -13,7 +15,6 @@ interface DetectSectionProps {
|
|
|
13
15
|
message?: string;
|
|
14
16
|
requestParams?: Array<{ label: string; value: string }>;
|
|
15
17
|
} | null | undefined;
|
|
16
|
-
onDetect: (instructions?: string, tone?: string, density?: number) => void | Promise<void>;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
// Color schemes are now handled via CSS data attributes
|
|
@@ -26,19 +27,24 @@ interface DetectSectionProps {
|
|
|
26
27
|
* - Optional tone selector (for comments)
|
|
27
28
|
* - Detect button with sparkle animation
|
|
28
29
|
* - Progress display during detection
|
|
30
|
+
*
|
|
31
|
+
* @emits detection:start - Start detection for annotation type. Payload: { motivation: Motivation, options: { instructions?: string, tone?: string, density?: number } }
|
|
32
|
+
* @emits detection:dismiss-progress - Dismiss the detection progress display
|
|
29
33
|
*/
|
|
30
34
|
export function DetectSection({
|
|
31
35
|
annotationType,
|
|
32
36
|
isDetecting,
|
|
33
37
|
detectionProgress,
|
|
34
|
-
onDetect
|
|
35
38
|
}: DetectSectionProps) {
|
|
39
|
+
|
|
36
40
|
const panelName = annotationType === 'highlight' ? 'HighlightPanel' :
|
|
37
41
|
annotationType === 'assessment' ? 'AssessmentPanel' :
|
|
38
42
|
'CommentsPanel';
|
|
39
43
|
const t = useTranslations(panelName);
|
|
44
|
+
const eventBus = useEventBus();
|
|
40
45
|
const [instructions, setInstructions] = useState('');
|
|
41
|
-
|
|
46
|
+
type ToneValue = 'scholarly' | 'explanatory' | 'conversational' | 'technical' | 'analytical' | 'critical' | 'balanced' | 'constructive' | '';
|
|
47
|
+
const [tone, setTone] = useState<ToneValue>('');
|
|
42
48
|
// Default density depends on annotation type
|
|
43
49
|
const defaultDensity = annotationType === 'comment' ? 5 : annotationType === 'assessment' ? 4 : annotationType === 'highlight' ? 5 : 5;
|
|
44
50
|
const [density, setDensity] = useState(defaultDensity);
|
|
@@ -57,16 +63,31 @@ export function DetectSection({
|
|
|
57
63
|
localStorage.setItem(`detect-section-expanded-${annotationType}`, String(isExpanded));
|
|
58
64
|
}, [isExpanded, annotationType]);
|
|
59
65
|
|
|
60
|
-
const handleDetect = () => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
const handleDetect = useCallback(() => {
|
|
67
|
+
// Map annotation type to motivation
|
|
68
|
+
const motivation: Motivation =
|
|
69
|
+
annotationType === 'highlight' ? 'highlighting' :
|
|
70
|
+
annotationType === 'assessment' ? 'assessing' :
|
|
71
|
+
'commenting';
|
|
72
|
+
|
|
73
|
+
// Emit detection:start event with options
|
|
74
|
+
eventBus.emit('detection:start', {
|
|
75
|
+
motivation,
|
|
76
|
+
options: {
|
|
77
|
+
instructions: instructions.trim() || undefined,
|
|
78
|
+
tone: (annotationType === 'comment' || annotationType === 'assessment') && tone ? tone : undefined,
|
|
79
|
+
density: (annotationType === 'comment' || annotationType === 'assessment' || annotationType === 'highlight') && useDensity ? density : undefined,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
66
83
|
setInstructions('');
|
|
67
84
|
setTone('');
|
|
68
85
|
// Don't reset density/useDensity - persist across detections
|
|
69
|
-
};
|
|
86
|
+
}, [annotationType, instructions, tone, useDensity, density]); // eventBus is stable singleton - never in deps
|
|
87
|
+
|
|
88
|
+
const handleDismissProgress = useCallback(() => {
|
|
89
|
+
eventBus.emit('detection:dismiss-progress', undefined);
|
|
90
|
+
}, []); // eventBus is stable singleton - never in deps
|
|
70
91
|
|
|
71
92
|
return (
|
|
72
93
|
<div className="semiont-panel__section">
|
|
@@ -91,7 +112,8 @@ export function DetectSection({
|
|
|
91
112
|
data-detecting={isDetecting && detectionProgress ? 'true' : 'false'}
|
|
92
113
|
data-type={annotationType}
|
|
93
114
|
>
|
|
94
|
-
{
|
|
115
|
+
{/* Show form when NOT detecting and NO progress to display */}
|
|
116
|
+
{!detectionProgress && (
|
|
95
117
|
<>
|
|
96
118
|
<div className="semiont-form-field">
|
|
97
119
|
<label className="semiont-form-field__label">
|
|
@@ -118,7 +140,7 @@ export function DetectSection({
|
|
|
118
140
|
</label>
|
|
119
141
|
<select
|
|
120
142
|
value={tone}
|
|
121
|
-
onChange={(e) => setTone(e.target.value)}
|
|
143
|
+
onChange={(e) => setTone(e.target.value as ToneValue)}
|
|
122
144
|
className="semiont-select"
|
|
123
145
|
>
|
|
124
146
|
<option value="">Default</option>
|
|
@@ -194,8 +216,8 @@ export function DetectSection({
|
|
|
194
216
|
</>
|
|
195
217
|
)}
|
|
196
218
|
|
|
197
|
-
{/* Detection Progress */}
|
|
198
|
-
{
|
|
219
|
+
{/* Detection Progress - show whenever we have progress (during or after detection) */}
|
|
220
|
+
{detectionProgress && (
|
|
199
221
|
<div className="semiont-detection-progress" data-type={annotationType}>
|
|
200
222
|
{/* Request Parameters */}
|
|
201
223
|
{detectionProgress.requestParams && detectionProgress.requestParams.length > 0 && (
|
|
@@ -214,6 +236,18 @@ export function DetectSection({
|
|
|
214
236
|
<span className="semiont-detection-progress__icon">✨</span>
|
|
215
237
|
<span>{detectionProgress.message}</span>
|
|
216
238
|
</div>
|
|
239
|
+
{/* Close button - shown after detection completes (when not actively detecting) */}
|
|
240
|
+
{!isDetecting && (
|
|
241
|
+
<button
|
|
242
|
+
onClick={handleDismissProgress}
|
|
243
|
+
className="semiont-detection-progress__close"
|
|
244
|
+
aria-label={t('closeProgress')}
|
|
245
|
+
title={t('closeProgress')}
|
|
246
|
+
type="button"
|
|
247
|
+
>
|
|
248
|
+
×
|
|
249
|
+
</button>
|
|
250
|
+
)}
|
|
217
251
|
</div>
|
|
218
252
|
</div>
|
|
219
253
|
)}
|
|
@@ -1,17 +1,16 @@
|
|
|
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
|
+
import { useEventBus } from '../../../contexts/EventBusContext';
|
|
6
7
|
|
|
7
8
|
type Annotation = components['schemas']['Annotation'];
|
|
8
9
|
|
|
9
10
|
interface HighlightEntryProps {
|
|
10
11
|
highlight: Annotation;
|
|
11
12
|
isFocused: boolean;
|
|
12
|
-
|
|
13
|
-
onHighlightRef: (highlightId: string, el: HTMLElement | null) => void;
|
|
14
|
-
onHighlightHover?: (highlightId: string | null) => void;
|
|
13
|
+
isHovered?: boolean;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
function formatRelativeTime(isoString: string): string {
|
|
@@ -31,41 +30,34 @@ function formatRelativeTime(isoString: string): string {
|
|
|
31
30
|
return date.toLocaleDateString();
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
onHighlightRef(highlight.id, highlightRef.current);
|
|
46
|
-
return () => {
|
|
47
|
-
onHighlightRef(highlight.id, null);
|
|
48
|
-
};
|
|
49
|
-
}, [highlight.id, onHighlightRef]);
|
|
50
|
-
|
|
51
|
-
// Scroll to highlight when focused
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
if (isFocused && highlightRef.current) {
|
|
54
|
-
highlightRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
55
|
-
}
|
|
56
|
-
}, [isFocused]);
|
|
33
|
+
export const HighlightEntry = forwardRef<HTMLDivElement, HighlightEntryProps>(
|
|
34
|
+
function HighlightEntry(
|
|
35
|
+
{
|
|
36
|
+
highlight,
|
|
37
|
+
isFocused,
|
|
38
|
+
isHovered = false,
|
|
39
|
+
},
|
|
40
|
+
ref
|
|
41
|
+
) {
|
|
42
|
+
const eventBus = useEventBus();
|
|
57
43
|
|
|
58
44
|
const selectedText = getAnnotationExactText(highlight);
|
|
59
45
|
|
|
60
46
|
return (
|
|
61
47
|
<div
|
|
62
|
-
ref={
|
|
63
|
-
className=
|
|
48
|
+
ref={ref}
|
|
49
|
+
className={`semiont-annotation-entry${isHovered ? ' semiont-annotation-pulse' : ''}`}
|
|
64
50
|
data-type="highlight"
|
|
65
51
|
data-focused={isFocused ? 'true' : 'false'}
|
|
66
|
-
onClick={
|
|
67
|
-
|
|
68
|
-
|
|
52
|
+
onClick={() => {
|
|
53
|
+
eventBus.emit('annotation:click', { annotationId: highlight.id, motivation: highlight.motivation });
|
|
54
|
+
}}
|
|
55
|
+
onMouseEnter={() => {
|
|
56
|
+
eventBus.emit('annotation:hover', { annotationId: highlight.id });
|
|
57
|
+
}}
|
|
58
|
+
onMouseLeave={() => {
|
|
59
|
+
eventBus.emit('annotation:hover', { annotationId: null });
|
|
60
|
+
}}
|
|
69
61
|
>
|
|
70
62
|
{/* Highlighted text */}
|
|
71
63
|
{selectedText && (
|
|
@@ -80,4 +72,4 @@ export function HighlightEntry({
|
|
|
80
72
|
</div>
|
|
81
73
|
</div>
|
|
82
74
|
);
|
|
83
|
-
}
|
|
75
|
+
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect } from 'react';
|
|
3
|
+
import { useEffect, useState, useRef, 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 { components, Selector } from '@semiont/api-client';
|
|
8
|
+
import { getTextPositionSelector, getTargetSelector } from '@semiont/api-client';
|
|
6
9
|
import { HighlightEntry } from './HighlightEntry';
|
|
7
|
-
import { useAnnotationPanel } from '../../../hooks/useAnnotationPanel';
|
|
8
10
|
import { DetectSection } from './DetectSection';
|
|
9
11
|
import { PanelHeader } from './PanelHeader';
|
|
10
12
|
import './HighlightPanel.css';
|
|
@@ -20,12 +22,6 @@ interface PendingAnnotation {
|
|
|
20
22
|
|
|
21
23
|
interface HighlightPanelProps {
|
|
22
24
|
annotations: Annotation[];
|
|
23
|
-
onAnnotationClick: (annotation: Annotation) => void;
|
|
24
|
-
focusedAnnotationId: string | null;
|
|
25
|
-
hoveredAnnotationId?: string | null;
|
|
26
|
-
onAnnotationHover?: (annotationId: string | null) => void;
|
|
27
|
-
onDetect?: (instructions?: string) => void | Promise<void>;
|
|
28
|
-
onCreate: (selector: Selector | Selector[]) => void;
|
|
29
25
|
pendingAnnotation: PendingAnnotation | null;
|
|
30
26
|
isDetecting?: boolean;
|
|
31
27
|
detectionProgress?: {
|
|
@@ -34,34 +30,115 @@ interface HighlightPanelProps {
|
|
|
34
30
|
message?: string;
|
|
35
31
|
} | null;
|
|
36
32
|
annotateMode?: boolean;
|
|
33
|
+
scrollToAnnotationId?: string | null;
|
|
34
|
+
onScrollCompleted?: () => void;
|
|
35
|
+
hoveredAnnotationId?: string | null;
|
|
37
36
|
}
|
|
38
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Panel for managing highlight annotations with auto-creation
|
|
40
|
+
*
|
|
41
|
+
* @emits annotation:create - Create new highlight annotation (auto-triggered). Payload: { motivation: 'highlighting', selector: Selector | Selector[], body: Body[] }
|
|
42
|
+
* @subscribes annotation:click - Annotation clicked. Payload: { annotationId: string }
|
|
43
|
+
*/
|
|
39
44
|
export function HighlightPanel({
|
|
40
45
|
annotations,
|
|
41
|
-
onAnnotationClick,
|
|
42
|
-
focusedAnnotationId,
|
|
43
|
-
hoveredAnnotationId,
|
|
44
|
-
onAnnotationHover,
|
|
45
|
-
onDetect,
|
|
46
|
-
onCreate,
|
|
47
46
|
pendingAnnotation,
|
|
48
47
|
isDetecting = false,
|
|
49
48
|
detectionProgress,
|
|
50
49
|
annotateMode = true,
|
|
50
|
+
scrollToAnnotationId,
|
|
51
|
+
onScrollCompleted,
|
|
52
|
+
hoveredAnnotationId,
|
|
51
53
|
}: HighlightPanelProps) {
|
|
54
|
+
|
|
52
55
|
const t = useTranslations('HighlightPanel');
|
|
56
|
+
const eventBus = useEventBus();
|
|
57
|
+
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
|
|
58
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
59
|
+
|
|
60
|
+
// Direct ref management
|
|
61
|
+
const entryRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
|
62
|
+
|
|
63
|
+
// Sort annotations by their position in the resource
|
|
64
|
+
const sortedAnnotations = useMemo(() => {
|
|
65
|
+
return [...annotations].sort((a, b) => {
|
|
66
|
+
const aSelector = getTextPositionSelector(getTargetSelector(a.target));
|
|
67
|
+
const bSelector = getTextPositionSelector(getTargetSelector(b.target));
|
|
68
|
+
if (!aSelector || !bSelector) return 0;
|
|
69
|
+
return aSelector.start - bSelector.start;
|
|
70
|
+
});
|
|
71
|
+
}, [annotations]);
|
|
72
|
+
|
|
73
|
+
// Ref callback for entry components
|
|
74
|
+
const setEntryRef = useCallback((id: string, element: HTMLDivElement | null) => {
|
|
75
|
+
if (element) {
|
|
76
|
+
entryRefs.current.set(id, element);
|
|
77
|
+
} else {
|
|
78
|
+
entryRefs.current.delete(id);
|
|
79
|
+
}
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
// Handle scrollToAnnotationId (click scroll)
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!scrollToAnnotationId) return;
|
|
85
|
+
const element = entryRefs.current.get(scrollToAnnotationId);
|
|
86
|
+
if (element && containerRef.current) {
|
|
87
|
+
const elementTop = element.offsetTop;
|
|
88
|
+
const containerHeight = containerRef.current.clientHeight;
|
|
89
|
+
const elementHeight = element.offsetHeight;
|
|
90
|
+
const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
|
|
91
|
+
containerRef.current.scrollTo({ top: scrollTo, behavior: 'smooth' });
|
|
92
|
+
element.classList.remove('semiont-annotation-pulse');
|
|
93
|
+
void element.offsetWidth;
|
|
94
|
+
element.classList.add('semiont-annotation-pulse');
|
|
95
|
+
if (onScrollCompleted) onScrollCompleted();
|
|
96
|
+
}
|
|
97
|
+
}, [scrollToAnnotationId]);
|
|
98
|
+
|
|
99
|
+
// Handle hoveredAnnotationId (hover scroll only - pulse is handled by isHovered prop)
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!hoveredAnnotationId) return;
|
|
102
|
+
const element = entryRefs.current.get(hoveredAnnotationId);
|
|
103
|
+
if (!element || !containerRef.current) return;
|
|
104
|
+
|
|
105
|
+
const container = containerRef.current;
|
|
106
|
+
const elementRect = element.getBoundingClientRect();
|
|
107
|
+
const containerRect = container.getBoundingClientRect();
|
|
108
|
+
const isVisible = elementRect.top >= containerRect.top && elementRect.bottom <= containerRect.bottom;
|
|
109
|
+
if (!isVisible) {
|
|
110
|
+
const elementTop = element.offsetTop;
|
|
111
|
+
const containerHeight = container.clientHeight;
|
|
112
|
+
const elementHeight = element.offsetHeight;
|
|
113
|
+
const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
|
|
114
|
+
container.scrollTo({ top: scrollTo, behavior: 'smooth' });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Pulse effect is handled by isHovered prop on HighlightEntry
|
|
118
|
+
}, [hoveredAnnotationId]);
|
|
119
|
+
|
|
120
|
+
// Subscribe to click events - update focused state
|
|
121
|
+
// Event handler for annotation clicks (extracted to avoid inline arrow function)
|
|
122
|
+
const handleAnnotationClick = useCallback(({ annotationId }: { annotationId: string }) => {
|
|
123
|
+
setFocusedAnnotationId(annotationId);
|
|
124
|
+
setTimeout(() => setFocusedAnnotationId(null), 3000);
|
|
125
|
+
}, []);
|
|
53
126
|
|
|
54
|
-
|
|
55
|
-
|
|
127
|
+
useEventSubscriptions({
|
|
128
|
+
'annotation:click': handleAnnotationClick,
|
|
129
|
+
});
|
|
56
130
|
|
|
57
131
|
// Highlights auto-create: when pendingAnnotation arrives with highlighting motivation,
|
|
58
|
-
// immediately
|
|
132
|
+
// immediately emit annotation:create event
|
|
59
133
|
useEffect(() => {
|
|
60
134
|
if (pendingAnnotation && pendingAnnotation.motivation === 'highlighting') {
|
|
61
|
-
|
|
135
|
+
eventBus.emit('annotation:create', {
|
|
136
|
+
motivation: 'highlighting',
|
|
137
|
+
selector: pendingAnnotation.selector,
|
|
138
|
+
body: [],
|
|
139
|
+
});
|
|
62
140
|
}
|
|
63
|
-
|
|
64
|
-
}, [pendingAnnotation]); // Only depend on pendingAnnotation, not onCreate (which is recreated on every render)
|
|
141
|
+
}, [pendingAnnotation]);
|
|
65
142
|
|
|
66
143
|
return (
|
|
67
144
|
<div className="semiont-panel">
|
|
@@ -70,12 +147,11 @@ export function HighlightPanel({
|
|
|
70
147
|
{/* Scrollable content area */}
|
|
71
148
|
<div ref={containerRef} className="semiont-panel__content">
|
|
72
149
|
{/* Detection Section - only in Annotate mode and for text resources */}
|
|
73
|
-
{annotateMode &&
|
|
150
|
+
{annotateMode && (
|
|
74
151
|
<DetectSection
|
|
75
152
|
annotationType="highlight"
|
|
76
153
|
isDetecting={isDetecting}
|
|
77
154
|
detectionProgress={detectionProgress}
|
|
78
|
-
onDetect={onDetect}
|
|
79
155
|
/>
|
|
80
156
|
)}
|
|
81
157
|
|
|
@@ -91,9 +167,8 @@ export function HighlightPanel({
|
|
|
91
167
|
key={highlight.id}
|
|
92
168
|
highlight={highlight}
|
|
93
169
|
isFocused={highlight.id === focusedAnnotationId}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
{...(onAnnotationHover && { onHighlightHover: onAnnotationHover })}
|
|
170
|
+
isHovered={highlight.id === hoveredAnnotationId}
|
|
171
|
+
ref={(el) => setEntryRef(highlight.id, el)}
|
|
97
172
|
/>
|
|
98
173
|
))
|
|
99
174
|
)}
|