@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.
Files changed (213) hide show
  1. package/dist/EventBusContext-CJjL_cCf.d.mts +462 -0
  2. package/dist/{PdfAnnotationCanvas.client-ADC4FFSE.mjs → PdfAnnotationCanvas.client-RAJRPQLU.mjs} +42 -27
  3. package/dist/PdfAnnotationCanvas.client-RAJRPQLU.mjs.map +1 -0
  4. package/dist/{ar-EMHEHPCJ.mjs → ar-4ZEORRW2.mjs} +7 -4
  5. package/dist/ar-4ZEORRW2.mjs.map +1 -0
  6. package/dist/{bn-OVCI4F6X.mjs → bn-SEDE5BQJ.mjs} +7 -4
  7. package/dist/bn-SEDE5BQJ.mjs.map +1 -0
  8. package/dist/{chunk-LIHZTECW.mjs → chunk-D7NBW4RV.mjs} +7 -4
  9. package/dist/chunk-D7NBW4RV.mjs.map +1 -0
  10. package/dist/{chunk-JZIO2A3B.mjs → chunk-QB52Q7EQ.mjs} +206 -146
  11. package/dist/chunk-QB52Q7EQ.mjs.map +1 -0
  12. package/dist/{cs-FAN66Q2F.mjs → cs-7W4WF5WD.mjs} +7 -4
  13. package/dist/cs-7W4WF5WD.mjs.map +1 -0
  14. package/dist/{da-YBBIHI2O.mjs → da-75XGBCBK.mjs} +7 -4
  15. package/dist/da-75XGBCBK.mjs.map +1 -0
  16. package/dist/{de-MAYU33LB.mjs → de-ODJVFLHM.mjs} +7 -4
  17. package/dist/de-ODJVFLHM.mjs.map +1 -0
  18. package/dist/{el-MKGSWN4O.mjs → el-C4PM4WB3.mjs} +7 -4
  19. package/dist/el-C4PM4WB3.mjs.map +1 -0
  20. package/dist/{en-DDLIXJCU.mjs → en-KJCJQ4OO.mjs} +2 -2
  21. package/dist/{es-52LHUWJD.mjs → es-WD33R7QL.mjs} +7 -4
  22. package/dist/es-WD33R7QL.mjs.map +1 -0
  23. package/dist/{fa-FJICRANB.mjs → fa-2BP6V56P.mjs} +7 -4
  24. package/dist/fa-2BP6V56P.mjs.map +1 -0
  25. package/dist/{fi-O455XFCR.mjs → fi-USRRW24J.mjs} +7 -4
  26. package/dist/fi-USRRW24J.mjs.map +1 -0
  27. package/dist/{fr-TXIXHOOE.mjs → fr-EC5S6WVF.mjs} +7 -4
  28. package/dist/fr-EC5S6WVF.mjs.map +1 -0
  29. package/dist/{he-JBSOX5IN.mjs → he-7TBVIKAA.mjs} +7 -4
  30. package/dist/he-7TBVIKAA.mjs.map +1 -0
  31. package/dist/{hi-KGHI3XVT.mjs → hi-FO4VIZLA.mjs} +7 -4
  32. package/dist/hi-FO4VIZLA.mjs.map +1 -0
  33. package/dist/{id-5OCPPZLO.mjs → id-7U7GGVWY.mjs} +7 -4
  34. package/dist/id-7U7GGVWY.mjs.map +1 -0
  35. package/dist/index.css +123 -85
  36. package/dist/index.css.map +1 -1
  37. package/dist/index.d.mts +715 -574
  38. package/dist/index.mjs +3898 -3575
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/{it-PNBBZSM2.mjs → it-Y4OPL6I2.mjs} +7 -4
  41. package/dist/it-Y4OPL6I2.mjs.map +1 -0
  42. package/dist/{ja-LDD7R3TJ.mjs → ja-PK7SQL55.mjs} +7 -4
  43. package/dist/ja-PK7SQL55.mjs.map +1 -0
  44. package/dist/{ko-F47ZDEY3.mjs → ko-L25PXMYD.mjs} +7 -4
  45. package/dist/ko-L25PXMYD.mjs.map +1 -0
  46. package/dist/{ms-Z7LMXJWL.mjs → ms-STH777QM.mjs} +7 -4
  47. package/dist/ms-STH777QM.mjs.map +1 -0
  48. package/dist/{nl-6SJFBPJ3.mjs → nl-Y7LECDDR.mjs} +7 -4
  49. package/dist/nl-Y7LECDDR.mjs.map +1 -0
  50. package/dist/{no-YXPBPSGF.mjs → no-KEKCEWU6.mjs} +7 -4
  51. package/dist/no-KEKCEWU6.mjs.map +1 -0
  52. package/dist/{pl-P4AZ2QME.mjs → pl-7A7OC75O.mjs} +7 -4
  53. package/dist/pl-7A7OC75O.mjs.map +1 -0
  54. package/dist/{pt-LHWUS6U6.mjs → pt-35HTM7RA.mjs} +7 -4
  55. package/dist/pt-35HTM7RA.mjs.map +1 -0
  56. package/dist/{ro-EA5J2ZON.mjs → ro-VAWL5KQA.mjs} +7 -4
  57. package/dist/ro-VAWL5KQA.mjs.map +1 -0
  58. package/dist/{sv-DATBS3UQ.mjs → sv-7ZK5EQEB.mjs} +7 -4
  59. package/dist/sv-7ZK5EQEB.mjs.map +1 -0
  60. package/dist/test-utils.d.mts +18 -8
  61. package/dist/test-utils.mjs +36 -14
  62. package/dist/test-utils.mjs.map +1 -1
  63. package/dist/{th-WTFJRWPT.mjs → th-UDWZ4X34.mjs} +7 -4
  64. package/dist/th-UDWZ4X34.mjs.map +1 -0
  65. package/dist/{tr-IKO3RXOX.mjs → tr-4WMPK3UX.mjs} +7 -4
  66. package/dist/tr-4WMPK3UX.mjs.map +1 -0
  67. package/dist/{uk-CF6CTTRK.mjs → uk-SSLASQYJ.mjs} +7 -4
  68. package/dist/uk-SSLASQYJ.mjs.map +1 -0
  69. package/dist/{vi-AJLTXPZQ.mjs → vi-IF42Z5PU.mjs} +7 -4
  70. package/dist/vi-IF42Z5PU.mjs.map +1 -0
  71. package/dist/{zh-U3ORHHYH.mjs → zh-HRQTNTAI.mjs} +7 -4
  72. package/dist/zh-HRQTNTAI.mjs.map +1 -0
  73. package/package.json +3 -1
  74. package/src/components/CodeMirrorRenderer.tsx +66 -93
  75. package/src/components/DetectionProgressWidget.tsx +16 -5
  76. package/src/components/ResizeHandle.tsx +10 -4
  77. package/src/components/SessionExpiryBanner.tsx +2 -3
  78. package/src/components/SessionTimer.tsx +3 -3
  79. package/src/components/Toolbar.tsx +18 -9
  80. package/src/components/__tests__/SessionTimer.test.tsx +33 -33
  81. package/src/components/annotation/AnnotateToolbar.tsx +17 -15
  82. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +165 -63
  83. package/src/components/annotation/annotation-entries.css +10 -0
  84. package/src/components/annotation-popups/JsonLdView.tsx +8 -2
  85. package/src/components/image-annotation/AnnotationOverlay.tsx +42 -22
  86. package/src/components/image-annotation/SvgDrawingCanvas.tsx +27 -30
  87. package/src/components/layout/__tests__/LeftSidebar.test.tsx +12 -33
  88. package/src/components/layout/__tests__/PageLayout.test.tsx +37 -32
  89. package/src/components/layout/__tests__/UnifiedHeader.test.tsx +21 -40
  90. package/src/components/modals/ResourceSearchModal.tsx +2 -2
  91. package/src/components/modals/SearchModal.tsx +1 -1
  92. package/src/components/navigation/CollapsibleResourceNavigation.tsx +14 -9
  93. package/src/components/navigation/NavigationTabs.css +36 -24
  94. package/src/components/navigation/ObservableLink.tsx +91 -0
  95. package/src/components/navigation/SimpleNavigation.tsx +20 -16
  96. package/src/components/navigation/SortableResourceTab.tsx +11 -5
  97. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +51 -26
  98. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +28 -22
  99. package/src/components/resource/AnnotateView.tsx +64 -134
  100. package/src/components/resource/BrowseView.tsx +86 -166
  101. package/src/components/resource/HistoryEvent.tsx +13 -7
  102. package/src/components/resource/ResourceViewer.tsx +122 -264
  103. package/src/components/resource/__tests__/BrowseView.test.tsx +631 -0
  104. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +231 -0
  105. package/src/components/resource/panels/AssessmentEntry.tsx +25 -33
  106. package/src/components/resource/panels/AssessmentPanel.tsx +106 -28
  107. package/src/components/resource/panels/CommentEntry.tsx +38 -32
  108. package/src/components/resource/panels/CommentsPanel.tsx +121 -28
  109. package/src/components/resource/panels/DetectSection.css +36 -1
  110. package/src/components/resource/panels/DetectSection.tsx +49 -15
  111. package/src/components/resource/panels/HighlightEntry.tsx +25 -33
  112. package/src/components/resource/panels/HighlightPanel.tsx +100 -25
  113. package/src/components/resource/panels/ReferenceEntry.tsx +61 -75
  114. package/src/components/resource/panels/ReferencesPanel.tsx +134 -42
  115. package/src/components/resource/panels/ResourceInfoPanel.tsx +47 -48
  116. package/src/components/resource/panels/TagEntry.tsx +25 -33
  117. package/src/components/resource/panels/TaggingPanel.tsx +118 -30
  118. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +30 -92
  119. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +129 -110
  120. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +86 -78
  121. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +144 -149
  122. package/src/components/resource/panels/__tests__/DetectSection.test.tsx +480 -0
  123. package/src/components/resource/panels/__tests__/HighlightPanel.detectionProgress.test.tsx +362 -0
  124. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +226 -111
  125. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +117 -61
  126. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +128 -106
  127. package/src/components/settings/SettingsPanel.tsx +15 -12
  128. package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +1 -46
  129. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -9
  130. package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -3
  131. package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -9
  132. package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -3
  133. package/src/features/admin-users/components/AdminUsersPage.tsx +0 -9
  134. package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -3
  135. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -9
  136. package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +0 -32
  137. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -9
  138. package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +0 -32
  139. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -9
  140. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +51 -54
  141. package/src/features/resource-compose/components/ResourceComposePage.tsx +3 -13
  142. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +39 -45
  143. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +9 -13
  144. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +234 -0
  145. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +234 -0
  146. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +388 -0
  147. package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +318 -0
  148. package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +503 -0
  149. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +139 -93
  150. package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +322 -0
  151. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +341 -524
  152. package/translations/ar.json +6 -3
  153. package/translations/bn.json +6 -3
  154. package/translations/cs.json +6 -3
  155. package/translations/da.json +6 -3
  156. package/translations/de.json +6 -3
  157. package/translations/el.json +6 -3
  158. package/translations/en.json +6 -3
  159. package/translations/es.json +6 -3
  160. package/translations/fa.json +6 -3
  161. package/translations/fi.json +6 -3
  162. package/translations/fr.json +6 -3
  163. package/translations/he.json +6 -3
  164. package/translations/hi.json +6 -3
  165. package/translations/id.json +6 -3
  166. package/translations/it.json +6 -3
  167. package/translations/ja.json +6 -3
  168. package/translations/ko.json +6 -3
  169. package/translations/ms.json +6 -3
  170. package/translations/nl.json +6 -3
  171. package/translations/no.json +6 -3
  172. package/translations/pl.json +6 -3
  173. package/translations/pt.json +6 -3
  174. package/translations/ro.json +6 -3
  175. package/translations/sv.json +6 -3
  176. package/translations/th.json +6 -3
  177. package/translations/tr.json +6 -3
  178. package/translations/uk.json +6 -3
  179. package/translations/vi.json +6 -3
  180. package/translations/zh.json +6 -3
  181. package/dist/PdfAnnotationCanvas.client-ADC4FFSE.mjs.map +0 -1
  182. package/dist/TranslationManager-Co_5fSxl.d.mts +0 -118
  183. package/dist/ar-EMHEHPCJ.mjs.map +0 -1
  184. package/dist/bn-OVCI4F6X.mjs.map +0 -1
  185. package/dist/chunk-JZIO2A3B.mjs.map +0 -1
  186. package/dist/chunk-LIHZTECW.mjs.map +0 -1
  187. package/dist/cs-FAN66Q2F.mjs.map +0 -1
  188. package/dist/da-YBBIHI2O.mjs.map +0 -1
  189. package/dist/de-MAYU33LB.mjs.map +0 -1
  190. package/dist/el-MKGSWN4O.mjs.map +0 -1
  191. package/dist/es-52LHUWJD.mjs.map +0 -1
  192. package/dist/fa-FJICRANB.mjs.map +0 -1
  193. package/dist/fi-O455XFCR.mjs.map +0 -1
  194. package/dist/fr-TXIXHOOE.mjs.map +0 -1
  195. package/dist/he-JBSOX5IN.mjs.map +0 -1
  196. package/dist/hi-KGHI3XVT.mjs.map +0 -1
  197. package/dist/id-5OCPPZLO.mjs.map +0 -1
  198. package/dist/it-PNBBZSM2.mjs.map +0 -1
  199. package/dist/ja-LDD7R3TJ.mjs.map +0 -1
  200. package/dist/ko-F47ZDEY3.mjs.map +0 -1
  201. package/dist/ms-Z7LMXJWL.mjs.map +0 -1
  202. package/dist/nl-6SJFBPJ3.mjs.map +0 -1
  203. package/dist/no-YXPBPSGF.mjs.map +0 -1
  204. package/dist/pl-P4AZ2QME.mjs.map +0 -1
  205. package/dist/pt-LHWUS6U6.mjs.map +0 -1
  206. package/dist/ro-EA5J2ZON.mjs.map +0 -1
  207. package/dist/sv-DATBS3UQ.mjs.map +0 -1
  208. package/dist/th-WTFJRWPT.mjs.map +0 -1
  209. package/dist/tr-IKO3RXOX.mjs.map +0 -1
  210. package/dist/uk-CF6CTTRK.mjs.map +0 -1
  211. package/dist/vi-AJLTXPZQ.mjs.map +0 -1
  212. package/dist/zh-U3ORHHYH.mjs.map +0 -1
  213. /package/dist/{en-DDLIXJCU.mjs.map → en-KJCJQ4OO.mjs.map} +0 -0
@@ -1,16 +1,17 @@
1
1
  /**
2
- * ResourceViewerPage - Pure React component for viewing resources
2
+ * ResourceViewerPage - Self-contained resource viewer component
3
3
  *
4
- * No Next.js dependencies - receives all data and callbacks via props.
5
- * This component handles the UI rendering and state management for the resource viewer.
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 type { components, ResourceUri, GenerationContext, Selector } from '@semiont/api-client';
10
- import { getLanguage, getPrimaryRepresentation, annotationUri, resourceUri, resourceAnnotationUri } from '@semiont/api-client';
11
- import { createCancelDetectionHandler, ANNOTATORS } from '@semiont/react-ui';
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 { MakeMeaningEventBusProvider, useMakeMeaningEvents } from '@semiont/react-ui';
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 frontend
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
- // Inner component that has access to event bus
158
- function ResourceViewerPageInner({
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 = useMakeMeaningEvents();
194
- // Resource loading announcements
195
- const {
196
- announceResourceLoading,
197
- announceResourceLoaded
198
- } = useResourceLoadingAnnouncements();
199
-
200
- // Derived state
201
- const documentEntityTypes = resource.entityTypes || [];
202
-
203
- // Get primary representation metadata
204
- const primaryRep = getPrimaryRepresentation(resource);
205
- const primaryMediaType = primaryRep?.mediaType;
206
- const primaryByteSize = primaryRep?.byteSize;
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
- // Annotate mode state - read-only copy for sidebar panel coordination
209
- // ResourceViewer manages the authoritative state and persists to localStorage
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
- // Unified annotation state (motivation-agnostic) - used by sidebar panels
218
- const [focusedAnnotationId, _setFocusedAnnotationId] = useState<string | null>(null);
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
- // Unified pending annotation - all human-created annotations flow through this
223
- const [pendingAnnotation, setPendingAnnotation] = useState<PendingAnnotation | null>(null);
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
- // Search state
226
- const [searchModalOpen, setSearchModalOpen] = useState(false);
227
- const [searchTerm, setSearchTerm] = useState('');
228
- const [pendingReferenceId, setPendingReferenceId] = useState<string | null>(null);
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
- // Generation config modal state
231
- const [generationModalOpen, setGenerationModalOpen] = useState(false);
232
- const [generationReferenceId, setGenerationReferenceId] = useState<string | null>(null);
233
- const [generationDefaultTitle, setGenerationDefaultTitle] = useState('');
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
- // Unified detection state (motivation-based)
236
- const [detectingMotivation, setDetectingMotivation] = useState<Motivation | null>(null);
237
- const [motivationDetectionProgress, setMotivationDetectionProgress] = useState<DetectionProgress | null>(null);
238
+ return {
239
+ ...annotation,
240
+ body: bodyArray,
241
+ };
242
+ }
243
+ return annotation;
244
+ }),
245
+ };
246
+ });
238
247
 
239
- // SSE stream reference for cancellation
240
- const detectionStreamRef = React.useRef<any>(null);
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
- // Handle event hover - trigger sparkle animation
243
- const handleEventHover = useCallback((annotationId: string | null) => {
244
- setHoveredAnnotationId(annotationId);
245
- if (annotationId) {
246
- onTriggerSparkleAnimation(annotationId);
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
- }, [onTriggerSparkleAnimation]);
290
+ }, [resources.update, rUri, refetchDocument, showSuccess, showError]);
249
291
 
250
- // Handle event click - scroll handled internally by ResourceViewer now
251
- const handleEventClick = useCallback((_annotationId: string | null) => {
252
- // ResourceViewer now manages scroll state internally
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
- // Use SSE-based document generation progress - provides inline sparkle animation
256
- const {
257
- progress: generationProgress,
258
- startGeneration,
259
- clearProgress
260
- } = useGenerationProgress({
261
- onComplete: () => {
262
- // Clear progress widget
263
- setTimeout(() => clearProgress(), 1000);
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
- // Generic detection context for all annotation types
271
- const detectionContext = {
272
- client,
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
- // Generic cancel handler (works for all detection types)
283
- const handleCancelDetection = useCallback(
284
- () => createCancelDetectionHandler({
285
- detectionStreamRef,
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
- // Handle document generation from stub reference
293
- const handleGenerateDocument = useCallback((
294
- referenceId: string,
295
- options: {
296
- title: string;
297
- prompt?: string;
298
- language?: string;
299
- temperature?: number;
300
- maxTokens?: number;
301
- context?: GenerationContext;
302
- }
303
- ) => {
304
- // Only open modal if this is the initial click (no context provided)
305
- if (!options.context) {
306
- setGenerationReferenceId(referenceId);
307
- setGenerationDefaultTitle(options.title);
308
- setGenerationModalOpen(true);
309
- return;
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
- // Modal submitted with full options including context - proceed with generation
313
- if (!resource) return;
314
-
315
- // Clear CSS sparkle animation if reference was recently created
316
- onClearNewAnnotationId(annotationUri(referenceId));
317
-
318
- // Use full resource URI (W3C Web Annotation spec requires URIs)
319
- const resourceUriStr = resource['@id'];
320
- startGeneration(annotationUri(referenceId), resourceUri(resourceUriStr), {
321
- ...options,
322
- // Use language from modal if provided, otherwise fall back to current locale
323
- language: options.language || locale,
324
- context: options.context
325
- });
326
- }, [startGeneration, resource, onClearNewAnnotationId, locale]);
327
-
328
- // Handle manual document creation from stub reference
329
- const handleCreateDocument = useCallback((
330
- annotationUri: string,
331
- title: string,
332
- entityTypes: string[]
333
- ) => {
334
- if (!resource) return;
335
-
336
- // Extract resource ID from URI
337
- const resourceId = rUri.split('/').pop() || '';
338
-
339
- // Navigate to compose page with reference context
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
- // Unified annotation request handler - all human-created annotations flow through this
369
- const handleAnnotationRequested = useCallback((pending: PendingAnnotation) => {
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
- // Cache invalidation now handled by annotation:added event
397
+ // Get primary representation metadata
398
+ const primaryRep = getPrimaryRepresentation(resource);
399
+ const primaryMediaType = primaryRep?.mediaType;
400
+ const primaryByteSize = primaryRep?.byteSize;
563
401
 
564
- if (annotatorConfig.create.successMessage) {
565
- const message = annotatorConfig.create.successMessage.replace('{value}', args[1] || '');
566
- showSuccess(message);
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
- }, [pendingAnnotation, onCreateAnnotation, rUri, showSuccess, showError]);
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
- // handleAnnotationClickAndFocus removed - ResourceViewer now manages focus/click state internally
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
- annotations={groups}
643
- onAnnotationRequested={handleAnnotationRequested}
644
- generatingReferenceId={generationProgress?.referenceId ?? null}
645
- showLineNumbers={showLineNumbers}
646
- annotators={ANNOTATORS}
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={motivationDetectionProgress}
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
- await onUpdateAnnotationBody(resourceAnnotationUri(nestedUri), {
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
- setSearchModalOpen(false);
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
- handleGenerateDocument(generationReferenceId, options);
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
- }