@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,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ResourceViewerPage -
|
|
2
|
+
* ResourceViewerPage - Self-contained resource viewer component
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Handles all data loading, event subscriptions, and side effects internally.
|
|
5
|
+
* Only requires minimal props from the framework layer (routing, modals).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React, { useState, useEffect, useCallback } from 'react';
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
8
|
+
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
9
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
10
|
+
import type { components, ResourceUri } from '@semiont/api-client';
|
|
11
|
+
import { getLanguage, getPrimaryRepresentation, resourceAnnotationUri, getPrimaryMediaType } from '@semiont/api-client';
|
|
12
|
+
import { uriToAnnotationId } from '@semiont/core';
|
|
13
|
+
import { ANNOTATORS } from '@semiont/react-ui';
|
|
12
14
|
import { ErrorBoundary } from '@semiont/react-ui';
|
|
13
|
-
import { useGenerationProgress } from '@semiont/react-ui';
|
|
14
15
|
import { AnnotationHistory } from '@semiont/react-ui';
|
|
15
16
|
import { UnifiedAnnotationsPanel } from '@semiont/react-ui';
|
|
16
17
|
import { ResourceInfoPanel } from '@semiont/react-ui';
|
|
@@ -20,19 +21,28 @@ import { Toolbar } from '@semiont/react-ui';
|
|
|
20
21
|
import { useResourceLoadingAnnouncements } from '@semiont/react-ui';
|
|
21
22
|
import type { GenerationOptions } from '@semiont/react-ui';
|
|
22
23
|
import { ResourceViewer } from '@semiont/react-ui';
|
|
23
|
-
import {
|
|
24
|
+
import { QUERY_KEYS } from '../../../lib/query-keys';
|
|
25
|
+
import { useResources, useEntityTypes } from '../../../lib/api-hooks';
|
|
26
|
+
import { useResourceContent } from '../../../hooks/useResourceContent';
|
|
27
|
+
import { useToast } from '../../../components/Toast';
|
|
28
|
+
import { useTheme } from '../../../contexts/ThemeContext';
|
|
29
|
+
import { useLineNumbers } from '../../../hooks/useLineNumbers';
|
|
30
|
+
import { useResourceEvents } from '../../../hooks/useResourceEvents';
|
|
31
|
+
import { useDebouncedCallback } from '../../../hooks/useDebounce';
|
|
32
|
+
import { useOpenResources } from '../../../contexts/OpenResourcesContext';
|
|
33
|
+
// Import EventBus hooks directly from context to avoid mocking issues in tests
|
|
34
|
+
import { useEventBus } from '../../../contexts/EventBusContext';
|
|
35
|
+
import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
|
|
36
|
+
import { useResourceAnnotations } from '../../../contexts/ResourceAnnotationsContext';
|
|
37
|
+
import { useApiClient } from '../../../contexts/ApiClientContext';
|
|
38
|
+
import { useResolutionFlow } from '../../../contexts/useResolutionFlow';
|
|
39
|
+
import { useDetectionFlow } from '../../../hooks/useDetectionFlow';
|
|
40
|
+
import { usePanelNavigation } from '../../../hooks/usePanelNavigation';
|
|
41
|
+
import { useGenerationFlow } from '../../../hooks/useGenerationFlow';
|
|
42
|
+
import { useContextRetrievalFlow } from '../../../hooks/useContextRetrievalFlow';
|
|
24
43
|
|
|
25
44
|
type SemiontResource = components['schemas']['ResourceDescriptor'];
|
|
26
45
|
type Annotation = components['schemas']['Annotation'];
|
|
27
|
-
type Motivation = components['schemas']['Motivation'];
|
|
28
|
-
|
|
29
|
-
// Unified pending annotation type - all human-created annotations flow through this
|
|
30
|
-
interface PendingAnnotation {
|
|
31
|
-
selector: Selector | Selector[];
|
|
32
|
-
motivation: Motivation;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
import type { DetectionProgress } from '@semiont/react-ui';
|
|
36
46
|
|
|
37
47
|
export interface ResourceViewerPageProps {
|
|
38
48
|
/**
|
|
@@ -45,97 +55,16 @@ export interface ResourceViewerPageProps {
|
|
|
45
55
|
*/
|
|
46
56
|
rUri: ResourceUri;
|
|
47
57
|
|
|
48
|
-
/**
|
|
49
|
-
* Document content (already loaded)
|
|
50
|
-
*/
|
|
51
|
-
content: string;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Whether content is still loading
|
|
55
|
-
*/
|
|
56
|
-
contentLoading: boolean;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* All annotations for this resource
|
|
60
|
-
*/
|
|
61
|
-
annotations: Annotation[];
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Resources that reference this resource
|
|
65
|
-
*/
|
|
66
|
-
referencedBy: any[];
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Whether referencedBy is loading
|
|
70
|
-
*/
|
|
71
|
-
referencedByLoading: boolean;
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* All available entity types
|
|
75
|
-
*/
|
|
76
|
-
allEntityTypes: string[];
|
|
77
|
-
|
|
78
58
|
/**
|
|
79
59
|
* Current locale
|
|
80
60
|
*/
|
|
81
61
|
locale: string;
|
|
82
62
|
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Theme state
|
|
86
|
-
*/
|
|
87
|
-
theme: any;
|
|
88
|
-
onThemeChange: (theme: any) => void;
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Line numbers state
|
|
92
|
-
*/
|
|
93
|
-
showLineNumbers: boolean;
|
|
94
|
-
onLineNumbersToggle: () => void;
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Active toolbar panel
|
|
98
|
-
*/
|
|
99
|
-
activePanel: any;
|
|
100
|
-
onPanelToggle: (panel: any) => void;
|
|
101
|
-
setActivePanel: (panel: any) => void;
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Callbacks for resource actions
|
|
105
|
-
*/
|
|
106
|
-
onArchive: () => Promise<void>;
|
|
107
|
-
onUnarchive: () => Promise<void>;
|
|
108
|
-
onClone: () => Promise<void>;
|
|
109
|
-
onUpdateAnnotationBody: (annotationUri: string, data: any) => Promise<void>;
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Annotation CRUD callbacks
|
|
113
|
-
*/
|
|
114
|
-
onCreateAnnotation: (
|
|
115
|
-
rUri: ResourceUri,
|
|
116
|
-
motivation: Motivation,
|
|
117
|
-
selector: any,
|
|
118
|
-
body: any[]
|
|
119
|
-
) => Promise<void>;
|
|
120
|
-
onTriggerSparkleAnimation: (annotationId: string) => void;
|
|
121
|
-
onClearNewAnnotationId: (annotationId: string) => void;
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Toast notifications
|
|
125
|
-
*/
|
|
126
|
-
showSuccess: (message: string) => void;
|
|
127
|
-
showError: (message: string) => void;
|
|
128
|
-
|
|
129
63
|
/**
|
|
130
64
|
* Cache manager for detection
|
|
131
65
|
*/
|
|
132
66
|
cacheManager: any;
|
|
133
67
|
|
|
134
|
-
/**
|
|
135
|
-
* API client
|
|
136
|
-
*/
|
|
137
|
-
client: any;
|
|
138
|
-
|
|
139
68
|
/**
|
|
140
69
|
* Link component for routing
|
|
141
70
|
*/
|
|
@@ -147,216 +76,313 @@ export interface ResourceViewerPageProps {
|
|
|
147
76
|
routes: any;
|
|
148
77
|
|
|
149
78
|
/**
|
|
150
|
-
* Component dependencies - passed from
|
|
79
|
+
* Component dependencies - passed from framework layer
|
|
151
80
|
*/
|
|
152
81
|
ToolbarPanels: React.ComponentType<any>;
|
|
153
82
|
SearchResourcesModal: React.ComponentType<any>;
|
|
154
83
|
GenerationConfigModal: React.ComponentType<any>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Callback to refetch document from parent
|
|
87
|
+
*/
|
|
88
|
+
refetchDocument: () => Promise<unknown>;
|
|
155
89
|
}
|
|
156
90
|
|
|
157
|
-
|
|
158
|
-
|
|
91
|
+
/**
|
|
92
|
+
* ResourceViewerPage - Main component
|
|
93
|
+
*
|
|
94
|
+
* Uses hooks directly (NO containers, NO render props, NO ResourceViewerPageContent wrapper)
|
|
95
|
+
*
|
|
96
|
+
* @emits navigation:router-push - Navigate to a resource or filtered view
|
|
97
|
+
* @emits annotation:sparkle - Trigger sparkle animation on an annotation
|
|
98
|
+
* @emits annotation:update-body - Update annotation body content
|
|
99
|
+
* @subscribes resource:archive - Archive the current resource
|
|
100
|
+
* @subscribes resource:unarchive - Unarchive the current resource
|
|
101
|
+
* @subscribes resource:clone - Clone the current resource
|
|
102
|
+
* @subscribes annotation:sparkle - Trigger sparkle animation
|
|
103
|
+
* @subscribes annotation:created - Annotation was created
|
|
104
|
+
* @subscribes annotation:deleted - Annotation was deleted
|
|
105
|
+
* @subscribes annotation:create-failed - Annotation creation failed
|
|
106
|
+
* @subscribes annotation:delete-failed - Annotation deletion failed
|
|
107
|
+
* @subscribes annotation:body-updated - Annotation body was updated
|
|
108
|
+
* @subscribes annotation:body-update-failed - Annotation body update failed
|
|
109
|
+
* @subscribes settings:theme-changed - UI theme changed
|
|
110
|
+
* @subscribes settings:line-numbers-toggled - Line numbers display toggled
|
|
111
|
+
* @subscribes detection:complete - Detection completed
|
|
112
|
+
* @subscribes detection:failed - Detection failed
|
|
113
|
+
* @subscribes generation:complete - Generation completed
|
|
114
|
+
* @subscribes generation:failed - Generation failed
|
|
115
|
+
* @subscribes navigation:reference-navigate - Navigate to a referenced document
|
|
116
|
+
* @subscribes navigation:entity-type-clicked - Navigate filtered by entity type
|
|
117
|
+
*/
|
|
118
|
+
export function ResourceViewerPage({
|
|
159
119
|
resource,
|
|
160
120
|
rUri,
|
|
161
|
-
content,
|
|
162
|
-
contentLoading,
|
|
163
|
-
annotations,
|
|
164
|
-
referencedBy,
|
|
165
|
-
referencedByLoading,
|
|
166
|
-
allEntityTypes,
|
|
167
121
|
locale,
|
|
168
|
-
theme,
|
|
169
|
-
onThemeChange,
|
|
170
|
-
showLineNumbers,
|
|
171
|
-
onLineNumbersToggle,
|
|
172
|
-
activePanel,
|
|
173
|
-
onPanelToggle,
|
|
174
|
-
setActivePanel,
|
|
175
|
-
onArchive,
|
|
176
|
-
onUnarchive,
|
|
177
|
-
onClone,
|
|
178
|
-
onUpdateAnnotationBody,
|
|
179
|
-
onCreateAnnotation,
|
|
180
|
-
onTriggerSparkleAnimation,
|
|
181
|
-
onClearNewAnnotationId,
|
|
182
|
-
showSuccess,
|
|
183
|
-
showError,
|
|
184
122
|
cacheManager,
|
|
185
|
-
client,
|
|
186
123
|
Link,
|
|
187
124
|
routes,
|
|
188
125
|
ToolbarPanels,
|
|
189
126
|
SearchResourcesModal,
|
|
190
127
|
GenerationConfigModal,
|
|
128
|
+
refetchDocument,
|
|
191
129
|
}: ResourceViewerPageProps) {
|
|
192
130
|
// Get unified event bus for subscribing to UI events
|
|
193
|
-
const eventBus =
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
} =
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
const
|
|
131
|
+
const eventBus = useEventBus();
|
|
132
|
+
const client = useApiClient();
|
|
133
|
+
const queryClient = useQueryClient();
|
|
134
|
+
|
|
135
|
+
// UI state hooks
|
|
136
|
+
const { showError, showSuccess } = useToast();
|
|
137
|
+
const { theme, setTheme } = useTheme();
|
|
138
|
+
const { showLineNumbers, toggleLineNumbers } = useLineNumbers();
|
|
139
|
+
const { addResource } = useOpenResources();
|
|
140
|
+
const { triggerSparkleAnimation, clearNewAnnotationId } = useResourceAnnotations();
|
|
141
|
+
|
|
142
|
+
// API hooks
|
|
143
|
+
const resources = useResources();
|
|
144
|
+
const entityTypesAPI = useEntityTypes();
|
|
145
|
+
|
|
146
|
+
// Load all data
|
|
147
|
+
const { content, loading: contentLoading } = useResourceContent(rUri, resource);
|
|
148
|
+
|
|
149
|
+
const { data: annotationsData } = resources.annotations.useQuery(rUri);
|
|
150
|
+
const annotations = useMemo(
|
|
151
|
+
() => annotationsData?.annotations || [],
|
|
152
|
+
[annotationsData?.annotations]
|
|
153
|
+
);
|
|
207
154
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const [annotateMode, _setAnnotateMode] = useState(() => {
|
|
211
|
-
if (typeof window !== 'undefined') {
|
|
212
|
-
return localStorage.getItem('annotateMode') === 'true';
|
|
213
|
-
}
|
|
214
|
-
return false;
|
|
215
|
-
});
|
|
155
|
+
const { data: referencedByData, isLoading: referencedByLoading } = resources.referencedBy.useQuery(rUri);
|
|
156
|
+
const referencedBy = referencedByData?.referencedBy || [];
|
|
216
157
|
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
const [hoveredAnnotationId, setHoveredAnnotationId] = useState<string | null>(null);
|
|
220
|
-
// scrollToAnnotationId removed - ResourceViewer now manages scroll state internally
|
|
158
|
+
const { data: entityTypesData } = entityTypesAPI.list.useQuery();
|
|
159
|
+
const allEntityTypes = (entityTypesData as { entityTypes: string[] } | undefined)?.entityTypes || [];
|
|
221
160
|
|
|
222
|
-
//
|
|
223
|
-
const
|
|
161
|
+
// Flow state hooks (NO CONTAINERS)
|
|
162
|
+
const { detectingMotivation, detectionProgress, pendingAnnotation, hoveredAnnotationId } = useDetectionFlow(rUri);
|
|
163
|
+
const { activePanel, scrollToAnnotationId, panelInitialTab, onScrollCompleted } = usePanelNavigation();
|
|
164
|
+
const { searchModalOpen, pendingReferenceId, onCloseSearchModal } = useResolutionFlow(eventBus, { client, resourceUri: rUri });
|
|
165
|
+
const {
|
|
166
|
+
generationProgress,
|
|
167
|
+
generationModalOpen,
|
|
168
|
+
generationReferenceId,
|
|
169
|
+
generationDefaultTitle,
|
|
170
|
+
onGenerateDocument,
|
|
171
|
+
onCloseGenerationModal,
|
|
172
|
+
} = useGenerationFlow(locale, rUri.split('/').pop() || '', showSuccess, showError, cacheManager, clearNewAnnotationId);
|
|
173
|
+
const { retrievalContext, retrievalLoading, retrievalError } = useContextRetrievalFlow(eventBus, { client, resourceUri: rUri });
|
|
174
|
+
|
|
175
|
+
// Debounced invalidation for real-time events
|
|
176
|
+
const debouncedInvalidateAnnotations = useDebouncedCallback(
|
|
177
|
+
() => {
|
|
178
|
+
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.annotations(rUri) });
|
|
179
|
+
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.events(rUri) });
|
|
180
|
+
},
|
|
181
|
+
500
|
|
182
|
+
);
|
|
224
183
|
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
184
|
+
// Add resource to open tabs when it loads
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (resource && rUri) {
|
|
187
|
+
const resourceIdSegment = rUri.split('/').pop() || '';
|
|
188
|
+
const mediaType = getPrimaryMediaType(resource);
|
|
189
|
+
addResource(resourceIdSegment, resource.name, mediaType || undefined);
|
|
190
|
+
if (typeof localStorage !== 'undefined') {
|
|
191
|
+
localStorage.setItem('lastViewedDocumentId', resourceIdSegment);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}, [resource, rUri, addResource]);
|
|
229
195
|
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
196
|
+
// Real-time document events (SSE)
|
|
197
|
+
useResourceEvents({
|
|
198
|
+
rUri,
|
|
199
|
+
autoConnect: true,
|
|
200
|
+
|
|
201
|
+
// Annotation events - use debounced invalidation to batch rapid updates
|
|
202
|
+
onAnnotationAdded: useCallback((_event: any) => {
|
|
203
|
+
debouncedInvalidateAnnotations();
|
|
204
|
+
}, [debouncedInvalidateAnnotations]),
|
|
205
|
+
|
|
206
|
+
onAnnotationRemoved: useCallback((_event: any) => {
|
|
207
|
+
debouncedInvalidateAnnotations();
|
|
208
|
+
}, [debouncedInvalidateAnnotations]),
|
|
209
|
+
|
|
210
|
+
onAnnotationBodyUpdated: useCallback((event: any) => {
|
|
211
|
+
// Optimistically update annotations cache with body operations
|
|
212
|
+
queryClient.setQueryData(QUERY_KEYS.resources.annotations(rUri), (old: any) => {
|
|
213
|
+
if (!old) return old;
|
|
214
|
+
return {
|
|
215
|
+
...old,
|
|
216
|
+
annotations: old.annotations.map((annotation: any) => {
|
|
217
|
+
const annotationIdSegment = uriToAnnotationId(annotation.id);
|
|
218
|
+
if (annotationIdSegment === event.payload.annotationId) {
|
|
219
|
+
let bodyArray = Array.isArray(annotation.body) ? [...annotation.body] : [];
|
|
220
|
+
|
|
221
|
+
for (const op of event.payload.operations || []) {
|
|
222
|
+
if (op.op === 'add') {
|
|
223
|
+
bodyArray.push(op.item);
|
|
224
|
+
} else if (op.op === 'remove') {
|
|
225
|
+
bodyArray = bodyArray.filter((item: any) =>
|
|
226
|
+
JSON.stringify(item) !== JSON.stringify(op.item)
|
|
227
|
+
);
|
|
228
|
+
} else if (op.op === 'replace') {
|
|
229
|
+
const index = bodyArray.findIndex((item: any) =>
|
|
230
|
+
JSON.stringify(item) === JSON.stringify(op.oldItem)
|
|
231
|
+
);
|
|
232
|
+
if (index !== -1) {
|
|
233
|
+
bodyArray[index] = op.newItem;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
234
237
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
+
return {
|
|
239
|
+
...annotation,
|
|
240
|
+
body: bodyArray,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return annotation;
|
|
244
|
+
}),
|
|
245
|
+
};
|
|
246
|
+
});
|
|
238
247
|
|
|
239
|
-
|
|
240
|
-
|
|
248
|
+
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.events(rUri) });
|
|
249
|
+
}, [queryClient, rUri]),
|
|
250
|
+
|
|
251
|
+
// Document status events
|
|
252
|
+
onDocumentArchived: useCallback((_event: any) => {
|
|
253
|
+
refetchDocument();
|
|
254
|
+
showSuccess('This document has been archived');
|
|
255
|
+
debouncedInvalidateAnnotations();
|
|
256
|
+
}, [refetchDocument, showSuccess, debouncedInvalidateAnnotations]),
|
|
257
|
+
|
|
258
|
+
onDocumentUnarchived: useCallback((_event: any) => {
|
|
259
|
+
refetchDocument();
|
|
260
|
+
showSuccess('This document has been unarchived');
|
|
261
|
+
debouncedInvalidateAnnotations();
|
|
262
|
+
}, [refetchDocument, showSuccess, debouncedInvalidateAnnotations]),
|
|
263
|
+
|
|
264
|
+
// Entity tag events
|
|
265
|
+
onEntityTagAdded: useCallback((_event: any) => {
|
|
266
|
+
refetchDocument();
|
|
267
|
+
debouncedInvalidateAnnotations();
|
|
268
|
+
}, [refetchDocument, debouncedInvalidateAnnotations]),
|
|
269
|
+
|
|
270
|
+
onEntityTagRemoved: useCallback((_event: any) => {
|
|
271
|
+
refetchDocument();
|
|
272
|
+
debouncedInvalidateAnnotations();
|
|
273
|
+
}, [refetchDocument, debouncedInvalidateAnnotations]),
|
|
274
|
+
|
|
275
|
+
onError: useCallback((error: any) => {
|
|
276
|
+
console.error('[RealTime] Event stream error:', error);
|
|
277
|
+
}, []),
|
|
278
|
+
});
|
|
241
279
|
|
|
242
|
-
//
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
280
|
+
// Event handlers extracted to useCallback (tenet: no inline handlers in useEventSubscriptions)
|
|
281
|
+
const handleResourceArchive = useCallback(async () => {
|
|
282
|
+
try {
|
|
283
|
+
await resources.update.useMutation().mutateAsync({ rUri, data: { archived: true } });
|
|
284
|
+
await refetchDocument();
|
|
285
|
+
showSuccess('Document archived');
|
|
286
|
+
} catch (err) {
|
|
287
|
+
console.error('Failed to archive document:', err);
|
|
288
|
+
showError('Failed to archive document');
|
|
247
289
|
}
|
|
248
|
-
}, [
|
|
290
|
+
}, [resources.update, rUri, refetchDocument, showSuccess, showError]);
|
|
249
291
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
292
|
+
const handleResourceUnarchive = useCallback(async () => {
|
|
293
|
+
try {
|
|
294
|
+
await resources.update.useMutation().mutateAsync({ rUri, data: { archived: false } });
|
|
295
|
+
await refetchDocument();
|
|
296
|
+
showSuccess('Document unarchived');
|
|
297
|
+
} catch (err) {
|
|
298
|
+
console.error('Failed to unarchive document:', err);
|
|
299
|
+
showError('Failed to unarchive document');
|
|
300
|
+
}
|
|
301
|
+
}, [resources.update, rUri, refetchDocument, showSuccess, showError]);
|
|
254
302
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
onError: (error) => {
|
|
266
|
-
console.error('[Generation] Error:', error);
|
|
303
|
+
const handleResourceClone = useCallback(async () => {
|
|
304
|
+
try {
|
|
305
|
+
const result = await resources.generateCloneToken.useMutation().mutateAsync(rUri);
|
|
306
|
+
const token = result.token;
|
|
307
|
+
const cloneUrl = `${typeof window !== 'undefined' ? window.location.origin : ''}/know/clone?token=${token}`;
|
|
308
|
+
await navigator.clipboard.writeText(cloneUrl);
|
|
309
|
+
showSuccess('Clone link copied to clipboard');
|
|
310
|
+
} catch (err) {
|
|
311
|
+
console.error('Failed to generate clone token:', err);
|
|
312
|
+
showError('Failed to generate clone link');
|
|
267
313
|
}
|
|
268
|
-
});
|
|
314
|
+
}, [resources.generateCloneToken, rUri, showSuccess, showError]);
|
|
269
315
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
rUri,
|
|
274
|
-
setDetectingMotivation,
|
|
275
|
-
setMotivationDetectionProgress,
|
|
276
|
-
detectionStreamRef,
|
|
277
|
-
cacheManager,
|
|
278
|
-
showSuccess,
|
|
279
|
-
showError
|
|
280
|
-
};
|
|
316
|
+
const handleAnnotationSparkle = useCallback(({ annotationId }: { annotationId: string }) => {
|
|
317
|
+
triggerSparkleAnimation(annotationId);
|
|
318
|
+
}, [triggerSparkleAnimation]);
|
|
281
319
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
()
|
|
285
|
-
|
|
286
|
-
setDetectingMotivation,
|
|
287
|
-
setMotivationDetectionProgress
|
|
288
|
-
})(),
|
|
289
|
-
[]
|
|
290
|
-
);
|
|
320
|
+
const handleAnnotationCreated = useCallback(({ annotation }: { annotation: { id: string } }) => {
|
|
321
|
+
triggerSparkleAnimation(annotation.id);
|
|
322
|
+
debouncedInvalidateAnnotations();
|
|
323
|
+
}, [triggerSparkleAnimation, debouncedInvalidateAnnotations]);
|
|
291
324
|
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
325
|
+
const handleAnnotationCreateFailed = useCallback(() => showError('Failed to create annotation'), [showError]);
|
|
326
|
+
const handleAnnotationDeleteFailed = useCallback(() => showError('Failed to delete annotation'), [showError]);
|
|
327
|
+
const handleAnnotationBodyUpdated = useCallback(() => {
|
|
328
|
+
// Success - optimistic update already applied via useResourceEvents
|
|
329
|
+
}, []);
|
|
330
|
+
const handleAnnotationBodyUpdateFailed = useCallback(() => showError('Failed to update annotation'), [showError]);
|
|
331
|
+
|
|
332
|
+
const handleSettingsThemeChanged = useCallback(({ theme }: { theme: any }) => setTheme(theme), [setTheme]);
|
|
333
|
+
|
|
334
|
+
const handleDetectionComplete = useCallback(() => {
|
|
335
|
+
showSuccess('Detection complete');
|
|
336
|
+
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.annotations(rUri) });
|
|
337
|
+
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.events(rUri) });
|
|
338
|
+
}, [showSuccess, queryClient, rUri]);
|
|
339
|
+
const handleDetectionFailed = useCallback(() => showError('Detection failed'), [showError]);
|
|
340
|
+
const handleGenerationComplete = useCallback(() => showSuccess('Document generated'), [showSuccess]);
|
|
341
|
+
const handleGenerationFailed = useCallback(() => showError('Failed to generate document'), [showError]);
|
|
342
|
+
|
|
343
|
+
const handleReferenceNavigate = useCallback(({ documentId }: { documentId: string }) => {
|
|
344
|
+
if (routes.resource) {
|
|
345
|
+
const path = routes.resource.replace('[resourceId]', encodeURIComponent(documentId));
|
|
346
|
+
eventBus.emit('navigation:router-push', { path, reason: 'reference-link' });
|
|
310
347
|
}
|
|
348
|
+
}, [routes.resource]); // eventBus is stable singleton - never in deps
|
|
311
349
|
|
|
312
|
-
|
|
313
|
-
if (
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const entityTypesStr = entityTypes.join(',');
|
|
341
|
-
const params = new URLSearchParams({
|
|
342
|
-
name: title, // Compose page expects 'name' parameter
|
|
343
|
-
annotationUri, // Pass full annotation URI, not just ID
|
|
344
|
-
sourceDocumentId: resourceId,
|
|
345
|
-
...(entityTypes.length > 0 ? { entityTypes: entityTypesStr } : {}),
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
window.location.href = `/know/compose?${params.toString()}`;
|
|
349
|
-
}, [resource, rUri]);
|
|
350
|
-
|
|
351
|
-
// Handle search for documents to link to reference
|
|
352
|
-
const handleSearchDocuments = useCallback((referenceId: string, searchTerm: string) => {
|
|
353
|
-
setPendingReferenceId(referenceId);
|
|
354
|
-
setSearchTerm(searchTerm);
|
|
355
|
-
setSearchModalOpen(true);
|
|
356
|
-
}, []);
|
|
350
|
+
const handleEntityTypeClicked = useCallback(({ entityType }: { entityType: string }) => {
|
|
351
|
+
if (routes.know) {
|
|
352
|
+
const path = `${routes.know}?entityType=${encodeURIComponent(entityType)}`;
|
|
353
|
+
eventBus.emit('navigation:router-push', { path, reason: 'entity-type-filter' });
|
|
354
|
+
}
|
|
355
|
+
}, [routes.know]); // eventBus is stable singleton - never in deps
|
|
356
|
+
|
|
357
|
+
// Event bus subscriptions (combined into single useEventSubscriptions call to prevent hook ordering issues)
|
|
358
|
+
useEventSubscriptions({
|
|
359
|
+
'resource:archive': handleResourceArchive,
|
|
360
|
+
'resource:unarchive': handleResourceUnarchive,
|
|
361
|
+
'resource:clone': handleResourceClone,
|
|
362
|
+
'annotation:sparkle': handleAnnotationSparkle,
|
|
363
|
+
'annotation:created': handleAnnotationCreated,
|
|
364
|
+
'annotation:deleted': debouncedInvalidateAnnotations,
|
|
365
|
+
'annotation:create-failed': handleAnnotationCreateFailed,
|
|
366
|
+
'annotation:delete-failed': handleAnnotationDeleteFailed,
|
|
367
|
+
'annotation:body-updated': handleAnnotationBodyUpdated,
|
|
368
|
+
'annotation:body-update-failed': handleAnnotationBodyUpdateFailed,
|
|
369
|
+
'settings:theme-changed': handleSettingsThemeChanged,
|
|
370
|
+
'settings:line-numbers-toggled': toggleLineNumbers,
|
|
371
|
+
'detection:complete': handleDetectionComplete,
|
|
372
|
+
'detection:failed': handleDetectionFailed,
|
|
373
|
+
'generation:complete': handleGenerationComplete,
|
|
374
|
+
'generation:failed': handleGenerationFailed,
|
|
375
|
+
'navigation:reference-navigate': handleReferenceNavigate,
|
|
376
|
+
'navigation:entity-type-clicked': handleEntityTypeClicked,
|
|
377
|
+
});
|
|
357
378
|
|
|
379
|
+
// Resource loading announcements
|
|
380
|
+
const {
|
|
381
|
+
announceResourceLoading,
|
|
382
|
+
announceResourceLoaded
|
|
383
|
+
} = useResourceLoadingAnnouncements();
|
|
358
384
|
|
|
359
|
-
// Announce content loading state changes
|
|
385
|
+
// Announce content loading state changes (app-level)
|
|
360
386
|
useEffect(() => {
|
|
361
387
|
if (contentLoading) {
|
|
362
388
|
announceResourceLoading(resource.name);
|
|
@@ -365,211 +391,21 @@ function ResourceViewerPageInner({
|
|
|
365
391
|
}
|
|
366
392
|
}, [contentLoading, content, resource.name, announceResourceLoading, announceResourceLoaded]);
|
|
367
393
|
|
|
368
|
-
//
|
|
369
|
-
const
|
|
370
|
-
// Route to appropriate panel tab based on motivation
|
|
371
|
-
const MOTIVATION_TO_TAB: Record<Motivation, string> = {
|
|
372
|
-
highlighting: 'annotations',
|
|
373
|
-
commenting: 'annotations',
|
|
374
|
-
assessing: 'annotations',
|
|
375
|
-
tagging: 'annotations',
|
|
376
|
-
linking: 'annotations',
|
|
377
|
-
bookmarking: 'annotations',
|
|
378
|
-
classifying: 'annotations',
|
|
379
|
-
describing: 'annotations',
|
|
380
|
-
editing: 'annotations',
|
|
381
|
-
identifying: 'annotations',
|
|
382
|
-
moderating: 'annotations',
|
|
383
|
-
questioning: 'annotations',
|
|
384
|
-
replying: 'annotations',
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
setActivePanel(MOTIVATION_TO_TAB[pending.motivation] || 'annotations');
|
|
388
|
-
setPendingAnnotation(pending);
|
|
389
|
-
}, [setActivePanel]);
|
|
390
|
-
|
|
391
|
-
// Subscribe to UI events from ResourceViewer
|
|
392
|
-
useEffect(() => {
|
|
393
|
-
const handleCommentRequested = (selection: any) => {
|
|
394
|
-
handleAnnotationRequested({
|
|
395
|
-
selector: {
|
|
396
|
-
type: 'TextQuoteSelector',
|
|
397
|
-
exact: selection.exact,
|
|
398
|
-
start: selection.start,
|
|
399
|
-
end: selection.end,
|
|
400
|
-
...(selection.prefix && { prefix: selection.prefix }),
|
|
401
|
-
...(selection.suffix && { suffix: selection.suffix })
|
|
402
|
-
},
|
|
403
|
-
motivation: 'commenting'
|
|
404
|
-
});
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
const handleTagRequested = (selection: any) => {
|
|
408
|
-
handleAnnotationRequested({
|
|
409
|
-
selector: {
|
|
410
|
-
type: 'TextQuoteSelector',
|
|
411
|
-
exact: selection.exact,
|
|
412
|
-
start: selection.start,
|
|
413
|
-
end: selection.end,
|
|
414
|
-
...(selection.prefix && { prefix: selection.prefix }),
|
|
415
|
-
...(selection.suffix && { suffix: selection.suffix })
|
|
416
|
-
},
|
|
417
|
-
motivation: 'tagging'
|
|
418
|
-
});
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
const handleAssessmentRequested = (selection: any) => {
|
|
422
|
-
handleAnnotationRequested({
|
|
423
|
-
selector: {
|
|
424
|
-
type: 'TextQuoteSelector',
|
|
425
|
-
exact: selection.exact,
|
|
426
|
-
start: selection.start,
|
|
427
|
-
end: selection.end,
|
|
428
|
-
...(selection.prefix && { prefix: selection.prefix }),
|
|
429
|
-
...(selection.suffix && { suffix: selection.suffix })
|
|
430
|
-
},
|
|
431
|
-
motivation: 'assessing'
|
|
432
|
-
});
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
const handleReferenceRequested = (selection: any) => {
|
|
436
|
-
// Build selector based on what's present in the selection
|
|
437
|
-
let selector: any;
|
|
438
|
-
|
|
439
|
-
if (selection.svgSelector) {
|
|
440
|
-
selector = {
|
|
441
|
-
type: 'SvgSelector',
|
|
442
|
-
value: selection.svgSelector
|
|
443
|
-
};
|
|
444
|
-
} else if (selection.fragmentSelector) {
|
|
445
|
-
selector = {
|
|
446
|
-
type: 'FragmentSelector',
|
|
447
|
-
value: selection.fragmentSelector,
|
|
448
|
-
...(selection.conformsTo && { conformsTo: selection.conformsTo })
|
|
449
|
-
};
|
|
450
|
-
} else {
|
|
451
|
-
selector = {
|
|
452
|
-
type: 'TextQuoteSelector',
|
|
453
|
-
exact: selection.exact,
|
|
454
|
-
start: selection.start,
|
|
455
|
-
end: selection.end,
|
|
456
|
-
...(selection.prefix && { prefix: selection.prefix }),
|
|
457
|
-
...(selection.suffix && { suffix: selection.suffix })
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
handleAnnotationRequested({
|
|
462
|
-
selector,
|
|
463
|
-
motivation: 'linking'
|
|
464
|
-
});
|
|
465
|
-
};
|
|
466
|
-
|
|
467
|
-
// Handle cancel pending annotation
|
|
468
|
-
const handleCancelPending = () => {
|
|
469
|
-
setPendingAnnotation(null);
|
|
470
|
-
};
|
|
471
|
-
|
|
472
|
-
eventBus.on('ui:selection:comment-requested', handleCommentRequested);
|
|
473
|
-
eventBus.on('ui:selection:tag-requested', handleTagRequested);
|
|
474
|
-
eventBus.on('ui:selection:assessment-requested', handleAssessmentRequested);
|
|
475
|
-
eventBus.on('ui:selection:reference-requested', handleReferenceRequested);
|
|
476
|
-
eventBus.on('ui:annotation:cancel-pending', handleCancelPending);
|
|
477
|
-
|
|
478
|
-
return () => {
|
|
479
|
-
eventBus.off('ui:selection:comment-requested', handleCommentRequested);
|
|
480
|
-
eventBus.off('ui:selection:tag-requested', handleTagRequested);
|
|
481
|
-
eventBus.off('ui:selection:assessment-requested', handleAssessmentRequested);
|
|
482
|
-
eventBus.off('ui:selection:reference-requested', handleReferenceRequested);
|
|
483
|
-
eventBus.off('ui:annotation:cancel-pending', handleCancelPending);
|
|
484
|
-
};
|
|
485
|
-
}, [eventBus, handleAnnotationRequested]);
|
|
486
|
-
|
|
487
|
-
// Manual tag creation handler
|
|
488
|
-
// Shared UI handlers - same across all annotation types
|
|
489
|
-
const handleAnnotationClick = useCallback((annotation: Annotation) => {
|
|
490
|
-
setHoveredAnnotationId(annotation.id);
|
|
491
|
-
setTimeout(() => setHoveredAnnotationId(null), 1500);
|
|
492
|
-
}, []);
|
|
493
|
-
|
|
494
|
-
const handleAnnotationHover = useCallback((annotationId: string | null) => {
|
|
495
|
-
setHoveredAnnotationId(annotationId);
|
|
496
|
-
}, []);
|
|
497
|
-
|
|
498
|
-
// Single generic annotation creation handler - reads config from ANNOTATORS
|
|
499
|
-
const handleCreateAnnotation = useCallback(async (
|
|
500
|
-
motivation: Motivation,
|
|
501
|
-
...args: any[]
|
|
502
|
-
) => {
|
|
503
|
-
if (!pendingAnnotation || pendingAnnotation.motivation !== motivation) return;
|
|
504
|
-
|
|
505
|
-
// Find the config for this motivation
|
|
506
|
-
const annotatorConfig = Object.values(ANNOTATORS).find(a => a.motivation === motivation);
|
|
507
|
-
if (!annotatorConfig) return;
|
|
508
|
-
|
|
509
|
-
try {
|
|
510
|
-
let body: any[] = [];
|
|
511
|
-
let selector = pendingAnnotation.selector;
|
|
512
|
-
|
|
513
|
-
// Build body based on config
|
|
514
|
-
switch (annotatorConfig.create.bodyBuilder) {
|
|
515
|
-
case 'empty':
|
|
516
|
-
// args[0] might be selector for highlight/assessment
|
|
517
|
-
if (args[0]) selector = args[0];
|
|
518
|
-
body = [];
|
|
519
|
-
break;
|
|
520
|
-
|
|
521
|
-
case 'text':
|
|
522
|
-
// args[0] is commentText
|
|
523
|
-
body = [{
|
|
524
|
-
type: 'TextualBody',
|
|
525
|
-
value: args[0],
|
|
526
|
-
format: 'text/plain',
|
|
527
|
-
purpose: 'commenting'
|
|
528
|
-
}];
|
|
529
|
-
break;
|
|
530
|
-
|
|
531
|
-
case 'entityTag':
|
|
532
|
-
// args[0] is optional entityType
|
|
533
|
-
if (args[0]) {
|
|
534
|
-
body = [{
|
|
535
|
-
type: 'TextualBody',
|
|
536
|
-
purpose: 'tagging',
|
|
537
|
-
value: args[0]
|
|
538
|
-
}];
|
|
539
|
-
}
|
|
540
|
-
break;
|
|
541
|
-
|
|
542
|
-
case 'dualTag':
|
|
543
|
-
// args[0] is schemaId, args[1] is category
|
|
544
|
-
body = [
|
|
545
|
-
{
|
|
546
|
-
type: 'TextualBody',
|
|
547
|
-
purpose: 'tagging',
|
|
548
|
-
value: args[1] // category
|
|
549
|
-
},
|
|
550
|
-
{
|
|
551
|
-
type: 'TextualBody',
|
|
552
|
-
purpose: 'classifying',
|
|
553
|
-
value: args[0] // schemaId
|
|
554
|
-
}
|
|
555
|
-
];
|
|
556
|
-
break;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
await onCreateAnnotation(rUri, motivation, selector, body);
|
|
560
|
-
setPendingAnnotation(null);
|
|
394
|
+
// Derived state
|
|
395
|
+
const documentEntityTypes = resource.entityTypes || [];
|
|
561
396
|
|
|
562
|
-
|
|
397
|
+
// Get primary representation metadata
|
|
398
|
+
const primaryRep = getPrimaryRepresentation(resource);
|
|
399
|
+
const primaryMediaType = primaryRep?.mediaType;
|
|
400
|
+
const primaryByteSize = primaryRep?.byteSize;
|
|
563
401
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
} catch (error) {
|
|
569
|
-
console.error(`Failed to create ${annotatorConfig.internalType}:`, error);
|
|
570
|
-
showError(`Failed to create ${annotatorConfig.displayName.toLowerCase()}`);
|
|
402
|
+
// Annotate mode state - local UI state only
|
|
403
|
+
const [annotateMode, _setAnnotateMode] = useState(() => {
|
|
404
|
+
if (typeof window !== 'undefined') {
|
|
405
|
+
return localStorage.getItem('annotateMode') === 'true';
|
|
571
406
|
}
|
|
572
|
-
|
|
407
|
+
return false;
|
|
408
|
+
});
|
|
573
409
|
|
|
574
410
|
// Group annotations by type using static ANNOTATORS
|
|
575
411
|
const result = {
|
|
@@ -595,7 +431,16 @@ function ResourceViewerPageInner({
|
|
|
595
431
|
// Combine resource with content
|
|
596
432
|
const resourceWithContent = { ...resource, content };
|
|
597
433
|
|
|
598
|
-
//
|
|
434
|
+
// Handlers for AnnotationHistory (legacy event-based interaction)
|
|
435
|
+
const handleEventHover = useCallback((annotationId: string | null) => {
|
|
436
|
+
if (annotationId) {
|
|
437
|
+
eventBus.emit('annotation:sparkle', { annotationId });
|
|
438
|
+
}
|
|
439
|
+
}, []); // eventBus is stable singleton - never in deps
|
|
440
|
+
|
|
441
|
+
const handleEventClick = useCallback((_annotationId: string | null) => {
|
|
442
|
+
// ResourceViewer now manages scroll state internally
|
|
443
|
+
}, []);
|
|
599
444
|
|
|
600
445
|
// Document rendering
|
|
601
446
|
return (
|
|
@@ -639,12 +484,11 @@ function ResourceViewerPageInner({
|
|
|
639
484
|
) : (
|
|
640
485
|
<ResourceViewer
|
|
641
486
|
resource={resourceWithContent}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
/>
|
|
487
|
+
annotations={groups}
|
|
488
|
+
generatingReferenceId={generationProgress?.referenceId ?? null}
|
|
489
|
+
showLineNumbers={showLineNumbers}
|
|
490
|
+
hoveredAnnotationId={hoveredAnnotationId}
|
|
491
|
+
/>
|
|
648
492
|
)}
|
|
649
493
|
</ErrorBoundary>
|
|
650
494
|
</div>
|
|
@@ -656,9 +500,7 @@ function ResourceViewerPageInner({
|
|
|
656
500
|
<ToolbarPanels
|
|
657
501
|
activePanel={activePanel}
|
|
658
502
|
theme={theme}
|
|
659
|
-
onThemeChange={onThemeChange}
|
|
660
503
|
showLineNumbers={showLineNumbers}
|
|
661
|
-
onLineNumbersToggle={onLineNumbersToggle}
|
|
662
504
|
width={
|
|
663
505
|
activePanel === 'jsonld' ? 'w-[600px]' :
|
|
664
506
|
activePanel === 'annotations' ? 'w-[400px]' :
|
|
@@ -679,26 +521,20 @@ function ResourceViewerPageInner({
|
|
|
679
521
|
<UnifiedAnnotationsPanel
|
|
680
522
|
annotations={annotations}
|
|
681
523
|
annotators={ANNOTATORS}
|
|
682
|
-
onCreateAnnotation={handleCreateAnnotation}
|
|
683
|
-
detectionContext={detectionContext}
|
|
684
|
-
focusedAnnotationId={focusedAnnotationId}
|
|
685
|
-
hoveredAnnotationId={hoveredAnnotationId}
|
|
686
|
-
onAnnotationClick={handleAnnotationClick}
|
|
687
|
-
onAnnotationHover={handleAnnotationHover}
|
|
688
524
|
annotateMode={annotateMode}
|
|
689
525
|
detectingMotivation={detectingMotivation}
|
|
690
|
-
detectionProgress={
|
|
526
|
+
detectionProgress={detectionProgress}
|
|
691
527
|
pendingAnnotation={pendingAnnotation}
|
|
692
528
|
allEntityTypes={allEntityTypes}
|
|
693
|
-
onGenerateDocument={handleGenerateDocument}
|
|
694
|
-
onCreateDocument={handleCreateDocument}
|
|
695
529
|
generatingReferenceId={generationProgress?.referenceId ?? null}
|
|
696
|
-
onSearchDocuments={handleSearchDocuments}
|
|
697
|
-
onCancelDetection={handleCancelDetection}
|
|
698
|
-
{...(primaryMediaType ? { mediaType: primaryMediaType } : {})}
|
|
699
530
|
referencedBy={referencedBy}
|
|
700
531
|
referencedByLoading={referencedByLoading}
|
|
701
532
|
resourceId={rUri.split('/').pop() || ''}
|
|
533
|
+
scrollToAnnotationId={scrollToAnnotationId}
|
|
534
|
+
hoveredAnnotationId={hoveredAnnotationId}
|
|
535
|
+
onScrollCompleted={onScrollCompleted}
|
|
536
|
+
initialTab={panelInitialTab?.tab as any}
|
|
537
|
+
initialTabGeneration={panelInitialTab?.generation}
|
|
702
538
|
Link={Link}
|
|
703
539
|
routes={routes}
|
|
704
540
|
/>
|
|
@@ -724,9 +560,6 @@ function ResourceViewerPageInner({
|
|
|
724
560
|
primaryMediaType={primaryMediaType}
|
|
725
561
|
primaryByteSize={primaryByteSize}
|
|
726
562
|
isArchived={resource.archived ?? false}
|
|
727
|
-
onClone={onClone}
|
|
728
|
-
onArchive={onArchive}
|
|
729
|
-
onUnarchive={onUnarchive}
|
|
730
563
|
/>
|
|
731
564
|
)}
|
|
732
565
|
|
|
@@ -749,7 +582,6 @@ function ResourceViewerPageInner({
|
|
|
749
582
|
context="document"
|
|
750
583
|
activePanel={activePanel}
|
|
751
584
|
isArchived={resource.archived ?? false}
|
|
752
|
-
onPanelToggle={onPanelToggle}
|
|
753
585
|
/>
|
|
754
586
|
</div>
|
|
755
587
|
</div>
|
|
@@ -757,10 +589,7 @@ function ResourceViewerPageInner({
|
|
|
757
589
|
{/* Search Resources Modal */}
|
|
758
590
|
<SearchResourcesModal
|
|
759
591
|
isOpen={searchModalOpen}
|
|
760
|
-
onClose={
|
|
761
|
-
setSearchModalOpen(false);
|
|
762
|
-
setPendingReferenceId(null);
|
|
763
|
-
}}
|
|
592
|
+
onClose={onCloseSearchModal}
|
|
764
593
|
onSelect={async (documentId: string) => {
|
|
765
594
|
if (pendingReferenceId) {
|
|
766
595
|
try {
|
|
@@ -772,7 +601,8 @@ function ResourceViewerPageInner({
|
|
|
772
601
|
const resourceIdSegment = rUri.split('/').pop() || '';
|
|
773
602
|
const nestedUri = `${window.location.origin}/resources/${resourceIdSegment}/annotations/${annotationIdShort}`;
|
|
774
603
|
|
|
775
|
-
|
|
604
|
+
eventBus.emit('annotation:update-body', {
|
|
605
|
+
annotationUri: resourceAnnotationUri(nestedUri),
|
|
776
606
|
resourceId: resourceIdSegment,
|
|
777
607
|
operations: [{
|
|
778
608
|
op: 'add',
|
|
@@ -785,42 +615,29 @@ function ResourceViewerPageInner({
|
|
|
785
615
|
});
|
|
786
616
|
showSuccess('Reference linked successfully');
|
|
787
617
|
// Cache invalidation now handled by annotation:updated event
|
|
788
|
-
|
|
789
|
-
setPendingReferenceId(null);
|
|
618
|
+
onCloseSearchModal();
|
|
790
619
|
} catch (error) {
|
|
791
620
|
console.error('Failed to link reference:', error);
|
|
792
621
|
showError('Failed to link reference');
|
|
793
622
|
}
|
|
794
623
|
}
|
|
795
624
|
}}
|
|
796
|
-
searchTerm={searchTerm}
|
|
797
625
|
/>
|
|
798
626
|
|
|
799
627
|
{/* Generation Config Modal */}
|
|
800
628
|
<GenerationConfigModal
|
|
801
629
|
isOpen={generationModalOpen}
|
|
802
|
-
onClose={
|
|
803
|
-
setGenerationModalOpen(false);
|
|
804
|
-
setGenerationReferenceId(null);
|
|
805
|
-
}}
|
|
630
|
+
onClose={onCloseGenerationModal}
|
|
806
631
|
onGenerate={(options: GenerationOptions) => {
|
|
807
632
|
if (generationReferenceId) {
|
|
808
|
-
|
|
633
|
+
onGenerateDocument(generationReferenceId, options);
|
|
809
634
|
}
|
|
810
635
|
}}
|
|
811
|
-
referenceId={generationReferenceId || ''}
|
|
812
|
-
resourceUri={rUri}
|
|
813
636
|
defaultTitle={generationDefaultTitle}
|
|
637
|
+
context={retrievalContext}
|
|
638
|
+
contextLoading={retrievalLoading}
|
|
639
|
+
contextError={retrievalError}
|
|
814
640
|
/>
|
|
815
641
|
</div>
|
|
816
642
|
);
|
|
817
643
|
}
|
|
818
|
-
|
|
819
|
-
// Outer component that wraps MakeMeaningEventBusProvider
|
|
820
|
-
export function ResourceViewerPage(props: ResourceViewerPageProps) {
|
|
821
|
-
return (
|
|
822
|
-
<MakeMeaningEventBusProvider rUri={props.rUri}>
|
|
823
|
-
<ResourceViewerPageInner {...props} />
|
|
824
|
-
</MakeMeaningEventBusProvider>
|
|
825
|
-
);
|
|
826
|
-
}
|