@semiont/react-ui 0.2.33-build.77 → 0.2.33-build.79
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/{ar-RNNSPLQB.mjs → ar-EMHEHPCJ.mjs} +2 -1
- package/dist/ar-EMHEHPCJ.mjs.map +1 -0
- package/dist/{bn-S2CDL7EC.mjs → bn-OVCI4F6X.mjs} +2 -1
- package/dist/bn-OVCI4F6X.mjs.map +1 -0
- package/dist/{chunk-35LLVRFK.mjs → chunk-JZIO2A3B.mjs} +31 -31
- package/dist/{chunk-UDX2Q35T.mjs → chunk-LIHZTECW.mjs} +2 -1
- package/dist/chunk-LIHZTECW.mjs.map +1 -0
- package/dist/{cs-RSV675WU.mjs → cs-FAN66Q2F.mjs} +2 -1
- package/dist/cs-FAN66Q2F.mjs.map +1 -0
- package/dist/{da-CHXNPWJC.mjs → da-YBBIHI2O.mjs} +2 -1
- package/dist/da-YBBIHI2O.mjs.map +1 -0
- package/dist/{de-KPEZ53D4.mjs → de-MAYU33LB.mjs} +2 -1
- package/dist/de-MAYU33LB.mjs.map +1 -0
- package/dist/{el-MW2BME5T.mjs → el-MKGSWN4O.mjs} +2 -1
- package/dist/el-MKGSWN4O.mjs.map +1 -0
- package/dist/{en-EVMIX24Y.mjs → en-DDLIXJCU.mjs} +2 -2
- package/dist/{es-HQ24NYS3.mjs → es-52LHUWJD.mjs} +2 -1
- package/dist/es-52LHUWJD.mjs.map +1 -0
- package/dist/{fa-W34LRLHG.mjs → fa-FJICRANB.mjs} +2 -1
- package/dist/fa-FJICRANB.mjs.map +1 -0
- package/dist/{fi-3U44IGOA.mjs → fi-O455XFCR.mjs} +2 -1
- package/dist/fi-O455XFCR.mjs.map +1 -0
- package/dist/{fr-N7DKX6NN.mjs → fr-TXIXHOOE.mjs} +2 -1
- package/dist/fr-TXIXHOOE.mjs.map +1 -0
- package/dist/{he-CS4WRXN3.mjs → he-JBSOX5IN.mjs} +2 -1
- package/dist/he-JBSOX5IN.mjs.map +1 -0
- package/dist/{hi-GJDY46KA.mjs → hi-KGHI3XVT.mjs} +2 -1
- package/dist/hi-KGHI3XVT.mjs.map +1 -0
- package/dist/{id-WAEZJK2Y.mjs → id-5OCPPZLO.mjs} +2 -1
- package/dist/id-5OCPPZLO.mjs.map +1 -0
- package/dist/index.d.mts +102 -106
- package/dist/index.mjs +1814 -1450
- package/dist/index.mjs.map +1 -1
- package/dist/{it-VDNDMZPU.mjs → it-PNBBZSM2.mjs} +2 -1
- package/dist/it-PNBBZSM2.mjs.map +1 -0
- package/dist/{ja-5PEH56J5.mjs → ja-LDD7R3TJ.mjs} +2 -1
- package/dist/ja-LDD7R3TJ.mjs.map +1 -0
- package/dist/{ko-JYPL3WVA.mjs → ko-F47ZDEY3.mjs} +2 -1
- package/dist/ko-F47ZDEY3.mjs.map +1 -0
- package/dist/{ms-5PZVW76T.mjs → ms-Z7LMXJWL.mjs} +2 -1
- package/dist/ms-Z7LMXJWL.mjs.map +1 -0
- package/dist/{nl-YXES36KM.mjs → nl-6SJFBPJ3.mjs} +2 -1
- package/dist/nl-6SJFBPJ3.mjs.map +1 -0
- package/dist/{no-XRA2UCQD.mjs → no-YXPBPSGF.mjs} +2 -1
- package/dist/no-YXPBPSGF.mjs.map +1 -0
- package/dist/{pl-WH6LJA5G.mjs → pl-P4AZ2QME.mjs} +2 -1
- package/dist/pl-P4AZ2QME.mjs.map +1 -0
- package/dist/{pt-7GAG57BM.mjs → pt-LHWUS6U6.mjs} +2 -1
- package/dist/pt-LHWUS6U6.mjs.map +1 -0
- package/dist/{ro-BTDDRB7N.mjs → ro-EA5J2ZON.mjs} +2 -1
- package/dist/ro-EA5J2ZON.mjs.map +1 -0
- package/dist/{sv-7V5C2IT4.mjs → sv-DATBS3UQ.mjs} +2 -1
- package/dist/sv-DATBS3UQ.mjs.map +1 -0
- package/dist/test-utils.mjs +2 -2
- package/dist/{th-LPKYLBX5.mjs → th-WTFJRWPT.mjs} +2 -1
- package/dist/th-WTFJRWPT.mjs.map +1 -0
- package/dist/{tr-DU4RQL4M.mjs → tr-IKO3RXOX.mjs} +2 -1
- package/dist/tr-IKO3RXOX.mjs.map +1 -0
- package/dist/{uk-36UHTDDI.mjs → uk-CF6CTTRK.mjs} +2 -1
- package/dist/uk-CF6CTTRK.mjs.map +1 -0
- package/dist/{vi-GDHOUZDH.mjs → vi-AJLTXPZQ.mjs} +2 -1
- package/dist/vi-AJLTXPZQ.mjs.map +1 -0
- package/dist/{zh-TYUID4XZ.mjs → zh-U3ORHHYH.mjs} +2 -1
- package/dist/zh-U3ORHHYH.mjs.map +1 -0
- package/package.json +6 -2
- package/src/components/resource/AnnotateView.tsx +0 -4
- package/src/components/resource/AnnotationHistory.tsx +12 -13
- package/src/components/resource/BrowseView.tsx +8 -16
- package/src/components/resource/HistoryEvent.tsx +3 -4
- package/src/components/resource/ResourceViewer.tsx +174 -201
- package/src/components/resource/event-formatting.ts +316 -0
- package/src/components/resource/panels/AssessmentPanel.tsx +37 -9
- package/src/components/resource/panels/CollaborationPanel.tsx +20 -13
- package/src/components/resource/panels/CommentsPanel.tsx +38 -9
- package/src/components/resource/panels/ReferencesPanel.tsx +39 -14
- package/src/components/resource/panels/StatisticsPanel.tsx +9 -19
- package/src/components/resource/panels/TaggingPanel.tsx +27 -0
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +28 -21
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +547 -0
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +10 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +10 -0
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +564 -0
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +8 -15
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +13 -6
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +147 -78
- package/src/styles/motivations/motivation-assessment.css +28 -0
- package/src/styles/patterns/panel-helpers.css +26 -0
- package/translations/ar.json +1 -0
- package/translations/bn.json +1 -0
- package/translations/cs.json +1 -0
- package/translations/da.json +1 -0
- package/translations/de.json +1 -0
- package/translations/el.json +1 -0
- package/translations/en.json +1 -0
- package/translations/es.json +1 -0
- package/translations/fa.json +1 -0
- package/translations/fi.json +1 -0
- package/translations/fr.json +1 -0
- package/translations/he.json +1 -0
- package/translations/hi.json +1 -0
- package/translations/id.json +1 -0
- package/translations/it.json +1 -0
- package/translations/ja.json +1 -0
- package/translations/ko.json +1 -0
- package/translations/ms.json +1 -0
- package/translations/nl.json +1 -0
- package/translations/no.json +1 -0
- package/translations/pl.json +1 -0
- package/translations/pt.json +1 -0
- package/translations/ro.json +1 -0
- package/translations/sv.json +1 -0
- package/translations/th.json +1 -0
- package/translations/tr.json +1 -0
- package/translations/uk.json +1 -0
- package/translations/vi.json +1 -0
- package/translations/zh.json +1 -0
- package/dist/ar-RNNSPLQB.mjs.map +0 -1
- package/dist/bn-S2CDL7EC.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/{chunk-35LLVRFK.mjs.map → chunk-JZIO2A3B.mjs.map} +0 -0
- /package/dist/{en-EVMIX24Y.mjs.map → en-DDLIXJCU.mjs.map} +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, {
|
|
3
|
+
import React, { useEffect, useRef } from 'react';
|
|
4
4
|
import { useTranslations } from '../../contexts/TranslationContext';
|
|
5
5
|
import type { RouteBuilder, LinkComponentProps } from '../../contexts/RoutingContext';
|
|
6
6
|
import { useResources } from '../../lib/api-hooks';
|
|
7
|
-
import
|
|
7
|
+
import type { ResourceUri } from '@semiont/api-client';
|
|
8
|
+
import type { StoredEvent } from '@semiont/core';
|
|
9
|
+
import { getAnnotationUriFromEvent } from '@semiont/core';
|
|
8
10
|
import { HistoryEvent } from './HistoryEvent';
|
|
9
11
|
|
|
10
12
|
interface Props {
|
|
@@ -36,17 +38,14 @@ export function AnnotationHistory({ rUri, hoveredAnnotationId, onEventHover, onE
|
|
|
36
38
|
|
|
37
39
|
// Sort events by oldest first (most recent at bottom)
|
|
38
40
|
// Filter out all job events - they're represented by annotation.body.updated events instead
|
|
39
|
-
const events =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
a.metadata.sequenceNumber - b.metadata.sequenceNumber
|
|
48
|
-
);
|
|
49
|
-
}, [eventsData]);
|
|
41
|
+
const events = !eventsData?.events ? [] : [...eventsData.events]
|
|
42
|
+
.filter((e: StoredEvent) => {
|
|
43
|
+
const eventType = e.event.type;
|
|
44
|
+
return eventType !== 'job.started' && eventType !== 'job.progress' && eventType !== 'job.completed';
|
|
45
|
+
})
|
|
46
|
+
.sort((a: StoredEvent, b: StoredEvent) =>
|
|
47
|
+
a.metadata.sequenceNumber - b.metadata.sequenceNumber
|
|
48
|
+
);
|
|
50
49
|
|
|
51
50
|
// Scroll to bottom when History is first shown or when events change
|
|
52
51
|
useEffect(() => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useEffect, useRef, useCallback, lazy, Suspense } from 'react';
|
|
4
4
|
import ReactMarkdown from 'react-markdown';
|
|
5
5
|
import remarkGfm from 'remark-gfm';
|
|
6
6
|
import { remarkAnnotations, type PreparedAnnotation } from '../../lib/remark-annotations';
|
|
@@ -85,24 +85,16 @@ export function BrowseView({
|
|
|
85
85
|
const onAnnotationHover = handlers?.onHover;
|
|
86
86
|
const onCommentHover = handlers?.onCommentHover;
|
|
87
87
|
|
|
88
|
-
const allAnnotations =
|
|
89
|
-
[...highlights, ...references, ...assessments, ...comments, ...tags],
|
|
90
|
-
[highlights, references, assessments, comments, tags]
|
|
91
|
-
);
|
|
88
|
+
const allAnnotations = [...highlights, ...references, ...assessments, ...comments, ...tags];
|
|
92
89
|
|
|
93
|
-
const preparedAnnotations =
|
|
94
|
-
prepareAnnotations(allAnnotations, annotators),
|
|
95
|
-
[allAnnotations, annotators]
|
|
96
|
-
);
|
|
90
|
+
const preparedAnnotations = prepareAnnotations(allAnnotations, annotators);
|
|
97
91
|
|
|
98
92
|
// Create a map of annotation ID -> full annotation for click handling
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return map;
|
|
105
|
-
}, [allAnnotations]);
|
|
93
|
+
const map = new Map<string, Annotation>();
|
|
94
|
+
for (const ann of allAnnotations) {
|
|
95
|
+
map.set(ann.id, ann);
|
|
96
|
+
}
|
|
97
|
+
const annotationMap = map;
|
|
106
98
|
|
|
107
99
|
// Wrapper for annotation hover that routes based on registry metadata
|
|
108
100
|
const handleAnnotationHover = useCallback((annotationId: string | null) => {
|
|
@@ -2,17 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useRef, useCallback } from 'react';
|
|
4
4
|
import type { RouteBuilder, LinkComponentProps } from '../../contexts/RoutingContext';
|
|
5
|
+
import type { StoredEvent, ResourceEventType } from '@semiont/core';
|
|
6
|
+
import { getAnnotationUriFromEvent } from '@semiont/core';
|
|
5
7
|
import {
|
|
6
|
-
type StoredEvent,
|
|
7
|
-
type ResourceEventType,
|
|
8
8
|
formatEventType,
|
|
9
9
|
getEventEmoji,
|
|
10
10
|
formatRelativeTime,
|
|
11
11
|
getEventDisplayContent,
|
|
12
12
|
getEventEntityTypes,
|
|
13
13
|
getResourceCreationDetails,
|
|
14
|
-
|
|
15
|
-
} from '@semiont/api-client';
|
|
14
|
+
} from './event-formatting';
|
|
16
15
|
|
|
17
16
|
type TranslateFn = (key: string, params?: Record<string, string | number>) => string;
|
|
18
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useState, useEffect, useCallback, useRef
|
|
3
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
4
|
// useRouter removed - using window.location for navigation
|
|
5
5
|
import { useTranslations } from '../../contexts/TranslationContext';
|
|
6
6
|
import { AnnotateView, type SelectionMotivation, type ClickAction, type ShapeType } from './AnnotateView';
|
|
@@ -10,6 +10,8 @@ import { JsonLdView } from '../annotation-popups/JsonLdView';
|
|
|
10
10
|
import type { components, Selector } from '@semiont/api-client';
|
|
11
11
|
import { getExactText, getTargetSelector, resourceUri, isHighlight, isAssessment, isReference, isComment, isTag, getBodySource } from '@semiont/api-client';
|
|
12
12
|
import { useResourceAnnotations } from '../../contexts/ResourceAnnotationsContext';
|
|
13
|
+
import { useMakeMeaningEvents } from '../../contexts/MakeMeaningEventBusContext';
|
|
14
|
+
import { useCacheManager } from '../../contexts/CacheContext';
|
|
13
15
|
import type { Annotator } from '../../lib/annotation-registry';
|
|
14
16
|
import type { AnnotationsCollection } from '../../types/annotation-props';
|
|
15
17
|
import { getSelectorType, getSelectedShapeForSelectorType, saveSelectedShapeForSelectorType } from '../../lib/media-shapes';
|
|
@@ -24,92 +26,46 @@ interface PendingAnnotation {
|
|
|
24
26
|
motivation: Motivation;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
/**
|
|
30
|
+
* ResourceViewer - Display and interact with resource content and annotations
|
|
31
|
+
*
|
|
32
|
+
* This component uses event-driven architecture for real-time updates:
|
|
33
|
+
* - Subscribes to make-meaning events (annotation:added, annotation:removed, annotation:updated)
|
|
34
|
+
* - Automatically invalidates cache when annotations change
|
|
35
|
+
* - No manual refetch needed - events handle cache invalidation
|
|
36
|
+
*
|
|
37
|
+
* Requirements:
|
|
38
|
+
* - Must be wrapped in MakeMeaningEventBusProvider (provides event bus)
|
|
39
|
+
* - Must be wrapped in CacheContext (provides cache manager)
|
|
40
|
+
*
|
|
41
|
+
* Event flow:
|
|
42
|
+
* make-meaning → EventLog → SSE → EventBus → ResourceViewer → Cache invalidation
|
|
43
|
+
*
|
|
44
|
+
* Phase 2 complete: Event-based cache invalidation replaces manual refetch
|
|
45
|
+
* Phase 3 complete: Fully event-driven - all user interactions use unified event bus
|
|
46
|
+
*/
|
|
27
47
|
interface Props {
|
|
28
48
|
resource: SemiontResource & { content: string };
|
|
29
49
|
annotations: AnnotationsCollection;
|
|
30
|
-
onRefetchAnnotations?: () => void;
|
|
31
|
-
annotateMode: boolean;
|
|
32
|
-
onAnnotateModeToggle: () => void;
|
|
33
50
|
generatingReferenceId?: string | null;
|
|
34
|
-
onAnnotationHover?: (annotationId: string | null) => void;
|
|
35
|
-
onCommentHover?: (commentId: string | null) => void;
|
|
36
|
-
hoveredAnnotationId?: string | null;
|
|
37
|
-
hoveredCommentId?: string | null;
|
|
38
|
-
scrollToAnnotationId?: string | null;
|
|
39
51
|
showLineNumbers?: boolean;
|
|
40
52
|
onAnnotationRequested?: (pending: PendingAnnotation) => void;
|
|
41
|
-
onCommentCreationRequested?: (selection: { exact: string; start: number; end: number; svgSelector?: string; fragmentSelector?: string; conformsTo?: string }) => void;
|
|
42
|
-
onTagCreationRequested?: (selection: { exact: string; start: number; end: number; svgSelector?: string; fragmentSelector?: string; conformsTo?: string }) => void;
|
|
43
|
-
onAssessmentCreationRequested?: (selection: { exact: string; start: number; end: number; svgSelector?: string; fragmentSelector?: string; conformsTo?: string }) => void;
|
|
44
|
-
onReferenceCreationRequested?: (selection: {
|
|
45
|
-
exact: string;
|
|
46
|
-
start: number;
|
|
47
|
-
end: number;
|
|
48
|
-
prefix?: string;
|
|
49
|
-
suffix?: string;
|
|
50
|
-
svgSelector?: string;
|
|
51
|
-
fragmentSelector?: string;
|
|
52
|
-
conformsTo?: string;
|
|
53
|
-
}) => void;
|
|
54
|
-
onCommentClick?: (commentId: string) => void;
|
|
55
|
-
onReferenceClick?: (referenceId: string) => void;
|
|
56
|
-
onHighlightClick?: (highlightId: string) => void;
|
|
57
|
-
onAssessmentClick?: (assessmentId: string) => void;
|
|
58
|
-
onTagClick?: (tagId: string) => void;
|
|
59
53
|
annotators: Record<string, Annotator>;
|
|
60
54
|
}
|
|
61
55
|
|
|
62
56
|
export function ResourceViewer({
|
|
63
57
|
resource,
|
|
64
58
|
annotations,
|
|
65
|
-
onRefetchAnnotations,
|
|
66
|
-
annotateMode,
|
|
67
|
-
onAnnotateModeToggle,
|
|
68
59
|
generatingReferenceId,
|
|
69
|
-
onAnnotationHover,
|
|
70
|
-
onCommentHover,
|
|
71
|
-
hoveredAnnotationId,
|
|
72
|
-
hoveredCommentId,
|
|
73
|
-
scrollToAnnotationId,
|
|
74
60
|
showLineNumbers = false,
|
|
75
61
|
onAnnotationRequested,
|
|
76
|
-
onCommentCreationRequested,
|
|
77
|
-
onTagCreationRequested,
|
|
78
|
-
onAssessmentCreationRequested,
|
|
79
|
-
onReferenceCreationRequested,
|
|
80
|
-
onCommentClick,
|
|
81
|
-
onReferenceClick,
|
|
82
|
-
onHighlightClick,
|
|
83
|
-
onAssessmentClick,
|
|
84
|
-
onTagClick,
|
|
85
62
|
annotators
|
|
86
63
|
}: Props) {
|
|
87
64
|
const t = useTranslations('ResourceViewer');
|
|
88
65
|
const documentViewerRef = useRef<HTMLDivElement>(null);
|
|
89
66
|
|
|
90
|
-
//
|
|
91
|
-
const
|
|
92
|
-
const onCommentCreationRequestedRef = useRef(onCommentCreationRequested);
|
|
93
|
-
const onTagCreationRequestedRef = useRef(onTagCreationRequested);
|
|
94
|
-
const onReferenceCreationRequestedRef = useRef(onReferenceCreationRequested);
|
|
95
|
-
const onCommentClickRef = useRef(onCommentClick);
|
|
96
|
-
const onReferenceClickRef = useRef(onReferenceClick);
|
|
97
|
-
const onHighlightClickRef = useRef(onHighlightClick);
|
|
98
|
-
const onAssessmentClickRef = useRef(onAssessmentClick);
|
|
99
|
-
const onTagClickRef = useRef(onTagClick);
|
|
100
|
-
|
|
101
|
-
// Keep refs up to date
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
onRefetchAnnotationsRef.current = onRefetchAnnotations;
|
|
104
|
-
onCommentCreationRequestedRef.current = onCommentCreationRequested;
|
|
105
|
-
onTagCreationRequestedRef.current = onTagCreationRequested;
|
|
106
|
-
onReferenceCreationRequestedRef.current = onReferenceCreationRequested;
|
|
107
|
-
onCommentClickRef.current = onCommentClick;
|
|
108
|
-
onReferenceClickRef.current = onReferenceClick;
|
|
109
|
-
onHighlightClickRef.current = onHighlightClick;
|
|
110
|
-
onAssessmentClickRef.current = onAssessmentClick;
|
|
111
|
-
onTagClickRef.current = onTagClick;
|
|
112
|
-
});
|
|
67
|
+
// Get unified event bus for emitting UI events
|
|
68
|
+
const eventBus = useMakeMeaningEvents();
|
|
113
69
|
|
|
114
70
|
const { highlights, references, assessments, comments, tags } = annotations;
|
|
115
71
|
|
|
@@ -131,13 +87,66 @@ export function ResourceViewer({
|
|
|
131
87
|
|
|
132
88
|
const mimeType = getMimeType();
|
|
133
89
|
|
|
134
|
-
//
|
|
90
|
+
// Annotate mode state - persisted in localStorage
|
|
91
|
+
const [annotateMode, setAnnotateMode] = useState<boolean>(() => {
|
|
92
|
+
if (typeof window !== 'undefined') {
|
|
93
|
+
return localStorage.getItem('annotateMode') === 'true';
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Persist annotateMode to localStorage
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (typeof window !== 'undefined') {
|
|
101
|
+
localStorage.setItem('annotateMode', annotateMode.toString());
|
|
102
|
+
}
|
|
103
|
+
}, [annotateMode]);
|
|
104
|
+
|
|
105
|
+
// Toggle handler
|
|
106
|
+
const toggleAnnotateMode = useCallback(() => {
|
|
107
|
+
setAnnotateMode(prev => !prev);
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
// Determine active view based on annotate mode
|
|
135
111
|
const activeView = annotateMode ? 'annotate' : 'browse';
|
|
136
112
|
const {
|
|
137
113
|
deleteAnnotation,
|
|
138
114
|
createAnnotation
|
|
139
115
|
} = useResourceAnnotations();
|
|
140
116
|
|
|
117
|
+
// Event-based cache invalidation - subscribe to make-meaning events
|
|
118
|
+
// This replaces manual onRefetchAnnotations calls with automatic updates
|
|
119
|
+
const cacheManager = useCacheManager();
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!eventBus || !cacheManager) return;
|
|
123
|
+
|
|
124
|
+
// Annotation events - invalidate cache when annotations change
|
|
125
|
+
const handleAnnotationAdded = () => {
|
|
126
|
+
cacheManager.invalidateAnnotations(rUri);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const handleAnnotationRemoved = () => {
|
|
130
|
+
cacheManager.invalidateAnnotations(rUri);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const handleAnnotationUpdated = () => {
|
|
134
|
+
cacheManager.invalidateAnnotations(rUri);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Subscribe to make-meaning annotation events
|
|
138
|
+
eventBus.on('annotation:added', handleAnnotationAdded);
|
|
139
|
+
eventBus.on('annotation:removed', handleAnnotationRemoved);
|
|
140
|
+
eventBus.on('annotation:updated', handleAnnotationUpdated);
|
|
141
|
+
|
|
142
|
+
// Cleanup subscriptions
|
|
143
|
+
return () => {
|
|
144
|
+
eventBus.off('annotation:added', handleAnnotationAdded);
|
|
145
|
+
eventBus.off('annotation:removed', handleAnnotationRemoved);
|
|
146
|
+
eventBus.off('annotation:updated', handleAnnotationUpdated);
|
|
147
|
+
};
|
|
148
|
+
}, [eventBus, cacheManager, rUri]);
|
|
149
|
+
|
|
141
150
|
// Annotation toolbar state - persisted in localStorage
|
|
142
151
|
const [selectedMotivation, setSelectedMotivation] = useState<SelectionMotivation | null>(() => {
|
|
143
152
|
if (typeof window !== 'undefined') {
|
|
@@ -204,8 +213,23 @@ export function ResourceViewer({
|
|
|
204
213
|
position: { x: number; y: number };
|
|
205
214
|
} | null>(null);
|
|
206
215
|
|
|
216
|
+
// Internal UI state for hover, focus, and scroll
|
|
217
|
+
const [hoveredAnnotationId, _setHoveredAnnotationId] = useState<string | null>(null);
|
|
218
|
+
const [hoveredCommentId, _setHoveredCommentId] = useState<string | null>(null);
|
|
219
|
+
const [scrollToAnnotationId, setScrollToAnnotationId] = useState<string | null>(null);
|
|
220
|
+
const [_focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
|
|
221
|
+
|
|
222
|
+
// Focus annotation helper
|
|
223
|
+
const focusAnnotation = useCallback((annotationId: string) => {
|
|
224
|
+
setFocusedAnnotationId(annotationId);
|
|
225
|
+
setScrollToAnnotationId(annotationId);
|
|
226
|
+
|
|
227
|
+
// Clear focus after 3 seconds
|
|
228
|
+
setTimeout(() => setFocusedAnnotationId(null), 3000);
|
|
229
|
+
}, []);
|
|
230
|
+
|
|
207
231
|
// Calculate centered position for JSON-LD modal
|
|
208
|
-
const
|
|
232
|
+
const getJsonLdModalPosition = () => {
|
|
209
233
|
if (typeof window === 'undefined') return { x: 0, y: 0 };
|
|
210
234
|
|
|
211
235
|
const popupWidth = 800;
|
|
@@ -215,13 +239,13 @@ export function ResourceViewer({
|
|
|
215
239
|
x: Math.max(0, (window.innerWidth - popupWidth) / 2),
|
|
216
240
|
y: Math.max(0, (window.innerHeight - popupHeight) / 2),
|
|
217
241
|
};
|
|
218
|
-
}
|
|
242
|
+
};
|
|
219
243
|
|
|
220
244
|
// Handle deleting annotations - memoized
|
|
221
245
|
const handleDeleteAnnotation = useCallback(async (id: string) => {
|
|
222
246
|
try {
|
|
223
247
|
await deleteAnnotation(id, rUri);
|
|
224
|
-
|
|
248
|
+
// Cache invalidation now handled by annotation:removed event
|
|
225
249
|
} catch (err) {
|
|
226
250
|
console.error('Failed to delete annotation:', err);
|
|
227
251
|
}
|
|
@@ -235,27 +259,9 @@ export function ResourceViewer({
|
|
|
235
259
|
// For delete/jsonld/follow modes, let those handlers below process it
|
|
236
260
|
if (metadata?.hasSidePanel) {
|
|
237
261
|
if (selectedClick === 'detail') {
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
if (isReference(annotation) && onReferenceClickRef.current) {
|
|
244
|
-
onReferenceClickRef.current(annotation.id);
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
if (isHighlight(annotation) && onHighlightClickRef.current) {
|
|
248
|
-
onHighlightClickRef.current(annotation.id);
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
if (isAssessment(annotation) && onAssessmentClickRef.current) {
|
|
252
|
-
onAssessmentClickRef.current(annotation.id);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
if (isTag(annotation) && onTagClickRef.current) {
|
|
256
|
-
onTagClickRef.current(annotation.id);
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
262
|
+
// Focus annotation (sets internal focus and scroll state, plus calls parent callback for backward compat)
|
|
263
|
+
focusAnnotation(annotation.id);
|
|
264
|
+
return;
|
|
259
265
|
}
|
|
260
266
|
// Don't return early for delete/jsonld/follow modes - let them be handled below
|
|
261
267
|
if (selectedClick !== 'deleting' && selectedClick !== 'jsonld' && selectedClick !== 'follow') {
|
|
@@ -299,7 +305,7 @@ export function ResourceViewer({
|
|
|
299
305
|
setDeleteConfirmation({ annotation, position });
|
|
300
306
|
return;
|
|
301
307
|
}
|
|
302
|
-
}, [annotateMode, selectedClick, handleDeleteAnnotation, annotators]);
|
|
308
|
+
}, [annotateMode, selectedClick, handleDeleteAnnotation, annotators, focusAnnotation]);
|
|
303
309
|
|
|
304
310
|
// Unified annotation creation handler - works for both text and images
|
|
305
311
|
const handleAnnotationCreate = useCallback(async (params: import('../../types/annotation-props').UICreateAnnotationParams) => {
|
|
@@ -335,13 +341,9 @@ export function ResourceViewer({
|
|
|
335
341
|
|
|
336
342
|
// Focus the new annotation to trigger panel tab switch
|
|
337
343
|
if (annotation) {
|
|
338
|
-
|
|
339
|
-
onHighlightClickRef.current(annotation.id);
|
|
340
|
-
} else if (motivation === 'assessing' && onAssessmentClickRef.current) {
|
|
341
|
-
onAssessmentClickRef.current(annotation.id);
|
|
342
|
-
}
|
|
344
|
+
focusAnnotation(annotation.id);
|
|
343
345
|
}
|
|
344
|
-
|
|
346
|
+
// Cache invalidation now handled by annotation:added event
|
|
345
347
|
} else if (selector.type === 'SvgSelector' && selector.value) {
|
|
346
348
|
// Image annotations use generic createAnnotation
|
|
347
349
|
await createAnnotation(
|
|
@@ -350,7 +352,7 @@ export function ResourceViewer({
|
|
|
350
352
|
{ type: 'SvgSelector', value: selector.value },
|
|
351
353
|
[]
|
|
352
354
|
);
|
|
353
|
-
|
|
355
|
+
// Cache invalidation now handled by annotation:added event
|
|
354
356
|
} else if (selector.type === 'FragmentSelector' && selector.value) {
|
|
355
357
|
// PDF annotations use FragmentSelector
|
|
356
358
|
await createAnnotation(
|
|
@@ -363,20 +365,18 @@ export function ResourceViewer({
|
|
|
363
365
|
},
|
|
364
366
|
[]
|
|
365
367
|
);
|
|
366
|
-
|
|
368
|
+
// Cache invalidation now handled by annotation:added event
|
|
367
369
|
}
|
|
368
370
|
break;
|
|
369
371
|
|
|
370
372
|
case 'commenting':
|
|
371
373
|
if (selector.type === 'TextQuoteSelector' && selector.exact) {
|
|
372
|
-
// Text:
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
});
|
|
379
|
-
}
|
|
374
|
+
// Text: emit UI event for comment creation
|
|
375
|
+
eventBus.emit('ui:selection:comment-requested', {
|
|
376
|
+
exact: selector.exact,
|
|
377
|
+
start: selector.start || 0,
|
|
378
|
+
end: selector.end || 0
|
|
379
|
+
});
|
|
380
380
|
} else if (selector.type === 'SvgSelector' && selector.value) {
|
|
381
381
|
// Image: create annotation, then open panel
|
|
382
382
|
const annotation = await createAnnotation(
|
|
@@ -385,10 +385,10 @@ export function ResourceViewer({
|
|
|
385
385
|
{ type: 'SvgSelector', value: selector.value },
|
|
386
386
|
[]
|
|
387
387
|
);
|
|
388
|
-
if (annotation
|
|
389
|
-
|
|
388
|
+
if (annotation) {
|
|
389
|
+
focusAnnotation(annotation.id);
|
|
390
390
|
}
|
|
391
|
-
|
|
391
|
+
// Cache invalidation now handled by annotation:added event
|
|
392
392
|
} else if (selector.type === 'FragmentSelector' && selector.value) {
|
|
393
393
|
// PDF: create annotation, then open panel
|
|
394
394
|
const annotation = await createAnnotation(
|
|
@@ -401,23 +401,21 @@ export function ResourceViewer({
|
|
|
401
401
|
},
|
|
402
402
|
[]
|
|
403
403
|
);
|
|
404
|
-
if (annotation
|
|
405
|
-
|
|
404
|
+
if (annotation) {
|
|
405
|
+
focusAnnotation(annotation.id);
|
|
406
406
|
}
|
|
407
|
-
|
|
407
|
+
// Cache invalidation now handled by annotation:added event
|
|
408
408
|
}
|
|
409
409
|
break;
|
|
410
410
|
|
|
411
411
|
case 'tagging':
|
|
412
412
|
if (selector.type === 'TextQuoteSelector' && selector.exact) {
|
|
413
|
-
// Text:
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
});
|
|
420
|
-
}
|
|
413
|
+
// Text: emit UI event for tag creation
|
|
414
|
+
eventBus.emit('ui:selection:tag-requested', {
|
|
415
|
+
exact: selector.exact,
|
|
416
|
+
start: selector.start || 0,
|
|
417
|
+
end: selector.end || 0
|
|
418
|
+
});
|
|
421
419
|
} else if (selector.type === 'SvgSelector' && selector.value) {
|
|
422
420
|
// Image: create annotation, then open panel
|
|
423
421
|
const annotation = await createAnnotation(
|
|
@@ -426,10 +424,10 @@ export function ResourceViewer({
|
|
|
426
424
|
{ type: 'SvgSelector', value: selector.value },
|
|
427
425
|
[]
|
|
428
426
|
);
|
|
429
|
-
if (annotation
|
|
430
|
-
|
|
427
|
+
if (annotation) {
|
|
428
|
+
focusAnnotation(annotation.id);
|
|
431
429
|
}
|
|
432
|
-
|
|
430
|
+
// Cache invalidation now handled by annotation:added event
|
|
433
431
|
} else if (selector.type === 'FragmentSelector' && selector.value) {
|
|
434
432
|
// PDF: create annotation, then open panel
|
|
435
433
|
const annotation = await createAnnotation(
|
|
@@ -442,43 +440,38 @@ export function ResourceViewer({
|
|
|
442
440
|
},
|
|
443
441
|
[]
|
|
444
442
|
);
|
|
445
|
-
if (annotation
|
|
446
|
-
|
|
443
|
+
if (annotation) {
|
|
444
|
+
focusAnnotation(annotation.id);
|
|
447
445
|
}
|
|
448
|
-
|
|
446
|
+
// Cache invalidation now handled by annotation:added event
|
|
449
447
|
}
|
|
450
448
|
break;
|
|
451
449
|
|
|
452
450
|
case 'linking':
|
|
453
|
-
//
|
|
454
|
-
if (
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
fragmentSelector: selector.value,
|
|
478
|
-
...(selector.conformsTo && { conformsTo: selector.conformsTo })
|
|
479
|
-
};
|
|
480
|
-
onReferenceCreationRequestedRef.current(selection);
|
|
481
|
-
}
|
|
451
|
+
// Emit UI event for reference creation (text, image, or PDF selections)
|
|
452
|
+
if (selector.type === 'TextQuoteSelector' && selector.exact) {
|
|
453
|
+
eventBus.emit('ui:selection:reference-requested', {
|
|
454
|
+
exact: selector.exact,
|
|
455
|
+
start: selector.start || 0,
|
|
456
|
+
end: selector.end || 0,
|
|
457
|
+
...(selector.prefix && { prefix: selector.prefix }),
|
|
458
|
+
...(selector.suffix && { suffix: selector.suffix })
|
|
459
|
+
});
|
|
460
|
+
} else if (selector.type === 'SvgSelector' && selector.value) {
|
|
461
|
+
eventBus.emit('ui:selection:reference-requested', {
|
|
462
|
+
exact: '', // Images don't have exact text
|
|
463
|
+
start: 0,
|
|
464
|
+
end: 0,
|
|
465
|
+
svgSelector: selector.value
|
|
466
|
+
});
|
|
467
|
+
} else if (selector.type === 'FragmentSelector' && selector.value) {
|
|
468
|
+
eventBus.emit('ui:selection:reference-requested', {
|
|
469
|
+
exact: '', // PDFs don't have exact text
|
|
470
|
+
start: 0,
|
|
471
|
+
end: 0,
|
|
472
|
+
fragmentSelector: selector.value,
|
|
473
|
+
...(selector.conformsTo && { conformsTo: selector.conformsTo })
|
|
474
|
+
});
|
|
482
475
|
}
|
|
483
476
|
break;
|
|
484
477
|
}
|
|
@@ -492,45 +485,29 @@ export function ResourceViewer({
|
|
|
492
485
|
await handleDeleteAnnotation(annotation.id);
|
|
493
486
|
}, [handleDeleteAnnotation]);
|
|
494
487
|
|
|
495
|
-
//
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
[highlights, references, assessments, comments, tags]
|
|
499
|
-
);
|
|
488
|
+
// Prepare props for child components
|
|
489
|
+
// Note: These objects are created inline - React's reconciliation handles re-renders efficiently
|
|
490
|
+
const annotationsCollection = { highlights, references, assessments, comments, tags };
|
|
500
491
|
|
|
501
|
-
const handlersForAnnotate =
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
...(onCommentHover && { onCommentHover })
|
|
506
|
-
}),
|
|
507
|
-
[handleAnnotationClick, onAnnotationHover, onCommentHover]
|
|
508
|
-
);
|
|
492
|
+
const handlersForAnnotate = {
|
|
493
|
+
onClick: handleAnnotationClick
|
|
494
|
+
// Note: onHover/onCommentHover removed - component now manages hover state internally
|
|
495
|
+
};
|
|
509
496
|
|
|
510
|
-
const handlersForBrowse =
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}),
|
|
515
|
-
[handleAnnotationClick, onCommentHover]
|
|
516
|
-
);
|
|
497
|
+
const handlersForBrowse = {
|
|
498
|
+
onClick: handleAnnotationClick
|
|
499
|
+
// Note: onCommentHover removed - component now manages hover state internally
|
|
500
|
+
};
|
|
517
501
|
|
|
518
|
-
const creationHandler =
|
|
519
|
-
() => ({ onCreate: handleAnnotationCreate }),
|
|
520
|
-
[handleAnnotationCreate]
|
|
521
|
-
);
|
|
502
|
+
const creationHandler = { onCreate: handleAnnotationCreate };
|
|
522
503
|
|
|
523
|
-
const uiState =
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
...(scrollToAnnotationId !== undefined && { scrollToAnnotationId })
|
|
531
|
-
}),
|
|
532
|
-
[selectedMotivation, selectedClick, selectedShape, hoveredAnnotationId, hoveredCommentId, scrollToAnnotationId]
|
|
533
|
-
);
|
|
504
|
+
const uiState = {
|
|
505
|
+
selectedMotivation,
|
|
506
|
+
selectedClick,
|
|
507
|
+
selectedShape,
|
|
508
|
+
hoveredAnnotationId,
|
|
509
|
+
scrollToAnnotationId
|
|
510
|
+
};
|
|
534
511
|
|
|
535
512
|
return (
|
|
536
513
|
<div ref={documentViewerRef} className="semiont-resource-viewer">
|
|
@@ -558,12 +535,8 @@ export function ResourceViewer({
|
|
|
558
535
|
onDeleteAnnotation={handleDeleteAnnotationWidget}
|
|
559
536
|
showLineNumbers={showLineNumbers}
|
|
560
537
|
annotateMode={annotateMode}
|
|
561
|
-
onAnnotateModeToggle={
|
|
538
|
+
onAnnotateModeToggle={toggleAnnotateMode}
|
|
562
539
|
{...(onAnnotationRequested && { onAnnotationRequested })}
|
|
563
|
-
{...(onCommentCreationRequested && { onCommentCreationRequested })}
|
|
564
|
-
{...(onTagCreationRequested && { onTagCreationRequested })}
|
|
565
|
-
{...(onAssessmentCreationRequested && { onAssessmentCreationRequested })}
|
|
566
|
-
{...(onReferenceCreationRequested && { onReferenceCreationRequested })}
|
|
567
540
|
annotators={annotators}
|
|
568
541
|
/>
|
|
569
542
|
) : (
|
|
@@ -573,11 +546,11 @@ export function ResourceViewer({
|
|
|
573
546
|
resourceUri={resource['@id']}
|
|
574
547
|
annotations={annotationsCollection}
|
|
575
548
|
handlers={handlersForBrowse}
|
|
576
|
-
|
|
549
|
+
hoveredCommentId={hoveredCommentId}
|
|
577
550
|
selectedClick={selectedClick}
|
|
578
551
|
onClickChange={setSelectedClick}
|
|
579
552
|
annotateMode={annotateMode}
|
|
580
|
-
onAnnotateModeToggle={
|
|
553
|
+
onAnnotateModeToggle={toggleAnnotateMode}
|
|
581
554
|
annotators={annotators}
|
|
582
555
|
/>
|
|
583
556
|
)}
|
|
@@ -587,7 +560,7 @@ export function ResourceViewer({
|
|
|
587
560
|
<PopupContainer
|
|
588
561
|
isOpen={showJsonLdView}
|
|
589
562
|
onClose={() => setShowJsonLdView(false)}
|
|
590
|
-
position={
|
|
563
|
+
position={getJsonLdModalPosition()}
|
|
591
564
|
wide={true}
|
|
592
565
|
>
|
|
593
566
|
<JsonLdView
|