@semiont/react-ui 0.2.33-build.78 → 0.2.33-build.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/dist/EventBusContext-7GvDyO0d.d.mts +414 -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-RNNSPLQB.mjs → ar-4ZEORRW2.mjs} +8 -4
  5. package/dist/ar-4ZEORRW2.mjs.map +1 -0
  6. package/dist/{bn-S2CDL7EC.mjs → bn-SEDE5BQJ.mjs} +8 -4
  7. package/dist/bn-SEDE5BQJ.mjs.map +1 -0
  8. package/dist/{chunk-UDX2Q35T.mjs → chunk-D7NBW4RV.mjs} +8 -4
  9. package/dist/chunk-D7NBW4RV.mjs.map +1 -0
  10. package/dist/{chunk-35LLVRFK.mjs → chunk-ZR4ZV2LY.mjs} +206 -146
  11. package/dist/chunk-ZR4ZV2LY.mjs.map +1 -0
  12. package/dist/{cs-RSV675WU.mjs → cs-7W4WF5WD.mjs} +8 -4
  13. package/dist/cs-7W4WF5WD.mjs.map +1 -0
  14. package/dist/{da-CHXNPWJC.mjs → da-75XGBCBK.mjs} +8 -4
  15. package/dist/da-75XGBCBK.mjs.map +1 -0
  16. package/dist/{de-KPEZ53D4.mjs → de-ODJVFLHM.mjs} +8 -4
  17. package/dist/de-ODJVFLHM.mjs.map +1 -0
  18. package/dist/{el-MW2BME5T.mjs → el-C4PM4WB3.mjs} +8 -4
  19. package/dist/el-C4PM4WB3.mjs.map +1 -0
  20. package/dist/{en-EVMIX24Y.mjs → en-KJCJQ4OO.mjs} +2 -2
  21. package/dist/{es-HQ24NYS3.mjs → es-WD33R7QL.mjs} +8 -4
  22. package/dist/es-WD33R7QL.mjs.map +1 -0
  23. package/dist/{fa-W34LRLHG.mjs → fa-2BP6V56P.mjs} +8 -4
  24. package/dist/fa-2BP6V56P.mjs.map +1 -0
  25. package/dist/{fi-3U44IGOA.mjs → fi-USRRW24J.mjs} +8 -4
  26. package/dist/fi-USRRW24J.mjs.map +1 -0
  27. package/dist/{fr-N7DKX6NN.mjs → fr-EC5S6WVF.mjs} +8 -4
  28. package/dist/fr-EC5S6WVF.mjs.map +1 -0
  29. package/dist/{he-CS4WRXN3.mjs → he-7TBVIKAA.mjs} +8 -4
  30. package/dist/he-7TBVIKAA.mjs.map +1 -0
  31. package/dist/{hi-GJDY46KA.mjs → hi-FO4VIZLA.mjs} +8 -4
  32. package/dist/hi-FO4VIZLA.mjs.map +1 -0
  33. package/dist/{id-WAEZJK2Y.mjs → id-7U7GGVWY.mjs} +8 -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 +699 -529
  38. package/dist/index.mjs +4291 -3491
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/{it-VDNDMZPU.mjs → it-Y4OPL6I2.mjs} +8 -4
  41. package/dist/it-Y4OPL6I2.mjs.map +1 -0
  42. package/dist/{ja-5PEH56J5.mjs → ja-PK7SQL55.mjs} +8 -4
  43. package/dist/ja-PK7SQL55.mjs.map +1 -0
  44. package/dist/{ko-JYPL3WVA.mjs → ko-L25PXMYD.mjs} +8 -4
  45. package/dist/ko-L25PXMYD.mjs.map +1 -0
  46. package/dist/{ms-5PZVW76T.mjs → ms-STH777QM.mjs} +8 -4
  47. package/dist/ms-STH777QM.mjs.map +1 -0
  48. package/dist/{nl-YXES36KM.mjs → nl-Y7LECDDR.mjs} +8 -4
  49. package/dist/nl-Y7LECDDR.mjs.map +1 -0
  50. package/dist/{no-XRA2UCQD.mjs → no-KEKCEWU6.mjs} +8 -4
  51. package/dist/no-KEKCEWU6.mjs.map +1 -0
  52. package/dist/{pl-WH6LJA5G.mjs → pl-7A7OC75O.mjs} +8 -4
  53. package/dist/pl-7A7OC75O.mjs.map +1 -0
  54. package/dist/{pt-7GAG57BM.mjs → pt-35HTM7RA.mjs} +8 -4
  55. package/dist/pt-35HTM7RA.mjs.map +1 -0
  56. package/dist/{ro-BTDDRB7N.mjs → ro-VAWL5KQA.mjs} +8 -4
  57. package/dist/ro-VAWL5KQA.mjs.map +1 -0
  58. package/dist/{sv-7V5C2IT4.mjs → sv-7ZK5EQEB.mjs} +8 -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-LPKYLBX5.mjs → th-UDWZ4X34.mjs} +8 -4
  64. package/dist/th-UDWZ4X34.mjs.map +1 -0
  65. package/dist/{tr-DU4RQL4M.mjs → tr-4WMPK3UX.mjs} +8 -4
  66. package/dist/tr-4WMPK3UX.mjs.map +1 -0
  67. package/dist/{uk-36UHTDDI.mjs → uk-SSLASQYJ.mjs} +8 -4
  68. package/dist/uk-SSLASQYJ.mjs.map +1 -0
  69. package/dist/{vi-GDHOUZDH.mjs → vi-IF42Z5PU.mjs} +8 -4
  70. package/dist/vi-IF42Z5PU.mjs.map +1 -0
  71. package/dist/{zh-TYUID4XZ.mjs → zh-HRQTNTAI.mjs} +8 -4
  72. package/dist/zh-HRQTNTAI.mjs.map +1 -0
  73. package/package.json +8 -2
  74. package/src/components/CodeMirrorRenderer.tsx +66 -93
  75. package/src/components/DetectionProgressWidget.tsx +16 -5
  76. package/src/components/LiveRegion.tsx +18 -18
  77. package/src/components/ResizeHandle.tsx +10 -4
  78. package/src/components/SessionTimer.tsx +2 -2
  79. package/src/components/Toolbar.tsx +18 -9
  80. package/src/components/__tests__/SessionTimer.test.tsx +9 -9
  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 -138
  100. package/src/components/resource/AnnotationHistory.tsx +12 -13
  101. package/src/components/resource/BrowseView.tsx +89 -177
  102. package/src/components/resource/HistoryEvent.tsx +16 -11
  103. package/src/components/resource/ResourceViewer.tsx +201 -370
  104. package/src/components/resource/__tests__/BrowseView.test.tsx +631 -0
  105. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +231 -0
  106. package/src/components/resource/event-formatting.ts +316 -0
  107. package/src/components/resource/panels/AssessmentEntry.tsx +25 -33
  108. package/src/components/resource/panels/AssessmentPanel.tsx +137 -31
  109. package/src/components/resource/panels/CollaborationPanel.tsx +20 -13
  110. package/src/components/resource/panels/CommentEntry.tsx +38 -32
  111. package/src/components/resource/panels/CommentsPanel.tsx +153 -31
  112. package/src/components/resource/panels/DetectSection.css +36 -1
  113. package/src/components/resource/panels/DetectSection.tsx +38 -10
  114. package/src/components/resource/panels/HighlightEntry.tsx +25 -33
  115. package/src/components/resource/panels/HighlightPanel.tsx +100 -25
  116. package/src/components/resource/panels/ReferenceEntry.tsx +61 -75
  117. package/src/components/resource/panels/ReferencesPanel.tsx +166 -49
  118. package/src/components/resource/panels/ResourceInfoPanel.tsx +47 -48
  119. package/src/components/resource/panels/StatisticsPanel.tsx +9 -19
  120. package/src/components/resource/panels/TagEntry.tsx +25 -33
  121. package/src/components/resource/panels/TaggingPanel.tsx +141 -25
  122. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +46 -101
  123. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +566 -0
  124. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +86 -78
  125. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +146 -141
  126. package/src/components/resource/panels/__tests__/DetectSection.test.tsx +480 -0
  127. package/src/components/resource/panels/__tests__/HighlightPanel.detectionProgress.test.tsx +362 -0
  128. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +228 -103
  129. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +117 -61
  130. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +586 -0
  131. package/src/components/settings/SettingsPanel.tsx +15 -12
  132. package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +1 -46
  133. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -9
  134. package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -3
  135. package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -9
  136. package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -3
  137. package/src/features/admin-users/components/AdminUsersPage.tsx +0 -9
  138. package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -3
  139. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -9
  140. package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +0 -32
  141. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -9
  142. package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +0 -32
  143. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -9
  144. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +51 -54
  145. package/src/features/resource-compose/components/ResourceComposePage.tsx +3 -13
  146. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +39 -45
  147. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +16 -27
  148. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +231 -0
  149. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +234 -0
  150. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +388 -0
  151. package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +318 -0
  152. package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +504 -0
  153. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +145 -91
  154. package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +322 -0
  155. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +325 -476
  156. package/src/styles/motivations/motivation-assessment.css +28 -0
  157. package/src/styles/patterns/panel-helpers.css +26 -0
  158. package/translations/ar.json +7 -3
  159. package/translations/bn.json +7 -3
  160. package/translations/cs.json +7 -3
  161. package/translations/da.json +7 -3
  162. package/translations/de.json +7 -3
  163. package/translations/el.json +7 -3
  164. package/translations/en.json +7 -3
  165. package/translations/es.json +7 -3
  166. package/translations/fa.json +7 -3
  167. package/translations/fi.json +7 -3
  168. package/translations/fr.json +7 -3
  169. package/translations/he.json +7 -3
  170. package/translations/hi.json +7 -3
  171. package/translations/id.json +7 -3
  172. package/translations/it.json +7 -3
  173. package/translations/ja.json +7 -3
  174. package/translations/ko.json +7 -3
  175. package/translations/ms.json +7 -3
  176. package/translations/nl.json +7 -3
  177. package/translations/no.json +7 -3
  178. package/translations/pl.json +7 -3
  179. package/translations/pt.json +7 -3
  180. package/translations/ro.json +7 -3
  181. package/translations/sv.json +7 -3
  182. package/translations/th.json +7 -3
  183. package/translations/tr.json +7 -3
  184. package/translations/uk.json +7 -3
  185. package/translations/vi.json +7 -3
  186. package/translations/zh.json +7 -3
  187. package/dist/PdfAnnotationCanvas.client-ADC4FFSE.mjs.map +0 -1
  188. package/dist/TranslationManager-Co_5fSxl.d.mts +0 -118
  189. package/dist/ar-RNNSPLQB.mjs.map +0 -1
  190. package/dist/bn-S2CDL7EC.mjs.map +0 -1
  191. package/dist/chunk-35LLVRFK.mjs.map +0 -1
  192. package/dist/chunk-UDX2Q35T.mjs.map +0 -1
  193. package/dist/cs-RSV675WU.mjs.map +0 -1
  194. package/dist/da-CHXNPWJC.mjs.map +0 -1
  195. package/dist/de-KPEZ53D4.mjs.map +0 -1
  196. package/dist/el-MW2BME5T.mjs.map +0 -1
  197. package/dist/es-HQ24NYS3.mjs.map +0 -1
  198. package/dist/fa-W34LRLHG.mjs.map +0 -1
  199. package/dist/fi-3U44IGOA.mjs.map +0 -1
  200. package/dist/fr-N7DKX6NN.mjs.map +0 -1
  201. package/dist/he-CS4WRXN3.mjs.map +0 -1
  202. package/dist/hi-GJDY46KA.mjs.map +0 -1
  203. package/dist/id-WAEZJK2Y.mjs.map +0 -1
  204. package/dist/it-VDNDMZPU.mjs.map +0 -1
  205. package/dist/ja-5PEH56J5.mjs.map +0 -1
  206. package/dist/ko-JYPL3WVA.mjs.map +0 -1
  207. package/dist/ms-5PZVW76T.mjs.map +0 -1
  208. package/dist/nl-YXES36KM.mjs.map +0 -1
  209. package/dist/no-XRA2UCQD.mjs.map +0 -1
  210. package/dist/pl-WH6LJA5G.mjs.map +0 -1
  211. package/dist/pt-7GAG57BM.mjs.map +0 -1
  212. package/dist/ro-BTDDRB7N.mjs.map +0 -1
  213. package/dist/sv-7V5C2IT4.mjs.map +0 -1
  214. package/dist/th-LPKYLBX5.mjs.map +0 -1
  215. package/dist/tr-DU4RQL4M.mjs.map +0 -1
  216. package/dist/uk-36UHTDDI.mjs.map +0 -1
  217. package/dist/vi-GDHOUZDH.mjs.map +0 -1
  218. package/dist/zh-TYUID4XZ.mjs.map +0 -1
  219. /package/dist/{en-EVMIX24Y.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
8
  import React, { useState, useEffect, useCallback, useMemo } 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';
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,18 +21,26 @@ 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';
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 '../../../hooks/useTheme';
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 { useDetectionFlow } from '../../../hooks/useDetectionFlow';
38
+ import { usePanelNavigation } from '../../../hooks/usePanelNavigation';
39
+ import { useAnnotationFlow } from '../../../hooks/useAnnotationFlow';
40
+ import { useGenerationFlow } from '../../../hooks/useGenerationFlow';
23
41
 
24
42
  type SemiontResource = components['schemas']['ResourceDescriptor'];
25
43
  type Annotation = components['schemas']['Annotation'];
26
- type Motivation = components['schemas']['Motivation'];
27
-
28
- // Unified pending annotation type - all human-created annotations flow through this
29
- interface PendingAnnotation {
30
- selector: Selector | Selector[];
31
- motivation: Motivation;
32
- }
33
-
34
- import type { DetectionProgress } from '@semiont/react-ui';
35
44
 
36
45
  export interface ResourceViewerPageProps {
37
46
  /**
@@ -44,98 +53,16 @@ export interface ResourceViewerPageProps {
44
53
  */
45
54
  rUri: ResourceUri;
46
55
 
47
- /**
48
- * Document content (already loaded)
49
- */
50
- content: string;
51
-
52
- /**
53
- * Whether content is still loading
54
- */
55
- contentLoading: boolean;
56
-
57
- /**
58
- * All annotations for this resource
59
- */
60
- annotations: Annotation[];
61
-
62
- /**
63
- * Resources that reference this resource
64
- */
65
- referencedBy: any[];
66
-
67
- /**
68
- * Whether referencedBy is loading
69
- */
70
- referencedByLoading: boolean;
71
-
72
- /**
73
- * All available entity types
74
- */
75
- allEntityTypes: string[];
76
-
77
56
  /**
78
57
  * Current locale
79
58
  */
80
59
  locale: string;
81
60
 
82
-
83
- /**
84
- * Theme state
85
- */
86
- theme: any;
87
- onThemeChange: (theme: any) => void;
88
-
89
- /**
90
- * Line numbers state
91
- */
92
- showLineNumbers: boolean;
93
- onLineNumbersToggle: () => void;
94
-
95
- /**
96
- * Active toolbar panel
97
- */
98
- activePanel: any;
99
- onPanelToggle: (panel: any) => void;
100
- setActivePanel: (panel: any) => void;
101
-
102
- /**
103
- * Callbacks for resource actions
104
- */
105
- onArchive: () => Promise<void>;
106
- onUnarchive: () => Promise<void>;
107
- onClone: () => Promise<void>;
108
- onUpdateAnnotationBody: (annotationUri: string, data: any) => Promise<void>;
109
- onRefetchAnnotations: () => 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
61
  /**
130
62
  * Cache manager for detection
131
63
  */
132
64
  cacheManager: any;
133
65
 
134
- /**
135
- * API client
136
- */
137
- client: any;
138
-
139
66
  /**
140
67
  * Link component for routing
141
68
  */
@@ -147,223 +74,279 @@ export interface ResourceViewerPageProps {
147
74
  routes: any;
148
75
 
149
76
  /**
150
- * Component dependencies - passed from frontend
77
+ * Component dependencies - passed from framework layer
151
78
  */
152
79
  ToolbarPanels: React.ComponentType<any>;
153
80
  SearchResourcesModal: React.ComponentType<any>;
154
81
  GenerationConfigModal: React.ComponentType<any>;
82
+
83
+ /**
84
+ * Callback to refetch document from parent
85
+ */
86
+ refetchDocument: () => Promise<unknown>;
155
87
  }
156
88
 
89
+ /**
90
+ * ResourceViewerPage - Main component
91
+ *
92
+ * Uses hooks directly (NO containers, NO render props, NO ResourceViewerPageContent wrapper)
93
+ */
157
94
  export function ResourceViewerPage({
158
95
  resource,
159
96
  rUri,
160
- content,
161
- contentLoading,
162
- annotations,
163
- referencedBy,
164
- referencedByLoading,
165
- allEntityTypes,
166
97
  locale,
167
- theme,
168
- onThemeChange,
169
- showLineNumbers,
170
- onLineNumbersToggle,
171
- activePanel,
172
- onPanelToggle,
173
- setActivePanel,
174
- onArchive,
175
- onUnarchive,
176
- onClone,
177
- onUpdateAnnotationBody,
178
- onRefetchAnnotations,
179
- onCreateAnnotation,
180
- onTriggerSparkleAnimation,
181
- onClearNewAnnotationId,
182
- showSuccess,
183
- showError,
184
98
  cacheManager,
185
- client,
186
99
  Link,
187
100
  routes,
188
101
  ToolbarPanels,
189
102
  SearchResourcesModal,
190
103
  GenerationConfigModal,
104
+ refetchDocument,
191
105
  }: ResourceViewerPageProps) {
192
- // Resource loading announcements
193
- const {
194
- announceResourceLoading,
195
- announceResourceLoaded
196
- } = useResourceLoadingAnnouncements();
197
-
198
- // Derived state
199
- const documentEntityTypes = resource.entityTypes || [];
200
-
201
- // Get primary representation metadata
202
- const primaryRep = getPrimaryRepresentation(resource);
203
- const primaryMediaType = primaryRep?.mediaType;
204
- const primaryByteSize = primaryRep?.byteSize;
205
-
206
- const [annotateMode, setAnnotateMode] = useState(() => {
207
- if (typeof window !== 'undefined') {
208
- return localStorage.getItem('annotateMode') === 'true';
209
- }
210
- return false;
211
- });
212
-
213
- // Unified annotation state (motivation-agnostic)
214
- const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
215
- const [hoveredAnnotationId, setHoveredAnnotationId] = useState<string | null>(null);
216
- const [scrollToAnnotationId, setScrollToAnnotationId] = useState<string | null>(null);
217
-
218
- // Unified pending annotation - all human-created annotations flow through this
219
- const [pendingAnnotation, setPendingAnnotation] = useState<PendingAnnotation | null>(null);
220
-
221
- // Search state
222
- const [searchModalOpen, setSearchModalOpen] = useState(false);
223
- const [searchTerm, setSearchTerm] = useState('');
224
- const [pendingReferenceId, setPendingReferenceId] = useState<string | null>(null);
225
-
226
- // Generation config modal state
227
- const [generationModalOpen, setGenerationModalOpen] = useState(false);
228
- const [generationReferenceId, setGenerationReferenceId] = useState<string | null>(null);
229
- const [generationDefaultTitle, setGenerationDefaultTitle] = useState('');
230
-
231
- // Unified detection state (motivation-based)
232
- const [detectingMotivation, setDetectingMotivation] = useState<Motivation | null>(null);
233
- const [motivationDetectionProgress, setMotivationDetectionProgress] = useState<DetectionProgress | null>(null);
234
-
235
- // SSE stream reference for cancellation
236
- const detectionStreamRef = React.useRef<any>(null);
237
-
238
- // Handle event hover - trigger sparkle animation
239
- const handleEventHover = useCallback((annotationId: string | null) => {
240
- setHoveredAnnotationId(annotationId);
241
- if (annotationId) {
242
- onTriggerSparkleAnimation(annotationId);
243
- }
244
- }, [onTriggerSparkleAnimation]);
106
+ // Get unified event bus for subscribing to UI events
107
+ const eventBus = useEventBus();
108
+ const queryClient = useQueryClient();
109
+
110
+ // UI state hooks
111
+ const { showError, showSuccess } = useToast();
112
+ const { theme, setTheme } = useTheme();
113
+ const { showLineNumbers, toggleLineNumbers } = useLineNumbers();
114
+ const { addResource } = useOpenResources();
115
+ const { triggerSparkleAnimation, clearNewAnnotationId } = useResourceAnnotations();
116
+
117
+ // API hooks
118
+ const resources = useResources();
119
+ const entityTypesAPI = useEntityTypes();
120
+
121
+ // Load all data
122
+ const { content, loading: contentLoading } = useResourceContent(rUri, resource);
123
+
124
+ const { data: annotationsData } = resources.annotations.useQuery(rUri);
125
+ const annotations = useMemo(
126
+ () => annotationsData?.annotations || [],
127
+ [annotationsData?.annotations]
128
+ );
245
129
 
246
- // Handle event click - scroll to annotation
247
- const handleEventClick = useCallback((annotationId: string | null) => {
248
- setScrollToAnnotationId(annotationId);
249
- }, []);
130
+ const { data: referencedByData, isLoading: referencedByLoading } = resources.referencedBy.useQuery(rUri);
131
+ const referencedBy = referencedByData?.referencedBy || [];
250
132
 
251
- // Handle annotate mode toggle - memoized
252
- const handleAnnotateModeToggle = useCallback(() => {
253
- const newMode = !annotateMode;
254
- setAnnotateMode(newMode);
255
- if (typeof window !== 'undefined') {
256
- localStorage.setItem('annotateMode', newMode.toString());
257
- }
258
- }, [annotateMode]);
133
+ const { data: entityTypesData } = entityTypesAPI.list.useQuery();
134
+ const allEntityTypes = (entityTypesData as { entityTypes: string[] } | undefined)?.entityTypes || [];
259
135
 
260
- // Use SSE-based document generation progress - provides inline sparkle animation
136
+ // Flow state hooks (NO CONTAINERS)
137
+ const { detectingMotivation, detectionProgress } = useDetectionFlow(rUri);
138
+ const { activePanel, scrollToAnnotationId, panelInitialTab, onScrollCompleted } = usePanelNavigation();
139
+ const { pendingAnnotation, hoveredAnnotationId } = useAnnotationFlow(rUri);
261
140
  const {
262
- progress: generationProgress,
263
- startGeneration,
264
- clearProgress
265
- } = useGenerationProgress({
266
- onComplete: () => {
267
- // Clear progress widget
268
- setTimeout(() => clearProgress(), 1000);
141
+ generationProgress,
142
+ generationModalOpen,
143
+ generationReferenceId,
144
+ generationDefaultTitle,
145
+ searchModalOpen,
146
+ pendingReferenceId,
147
+ onGenerateDocument,
148
+ onCloseGenerationModal,
149
+ onCloseSearchModal,
150
+ } = useGenerationFlow(locale, rUri.split('/').pop() || '', showSuccess, showError, cacheManager, clearNewAnnotationId);
151
+
152
+ // Debounced invalidation for real-time events
153
+ const debouncedInvalidateAnnotations = useDebouncedCallback(
154
+ () => {
155
+ queryClient.invalidateQueries({ queryKey: QUERY_KEYS.documents.annotations(rUri) });
156
+ queryClient.invalidateQueries({ queryKey: QUERY_KEYS.documents.events(rUri) });
269
157
  },
270
- onError: (error) => {
271
- console.error('[Generation] Error:', error);
158
+ 500
159
+ );
160
+
161
+ // Add resource to open tabs when it loads
162
+ useEffect(() => {
163
+ if (resource && rUri) {
164
+ const resourceIdSegment = rUri.split('/').pop() || '';
165
+ const mediaType = getPrimaryMediaType(resource);
166
+ addResource(resourceIdSegment, resource.name, mediaType || undefined);
167
+ if (typeof localStorage !== 'undefined') {
168
+ localStorage.setItem('lastViewedDocumentId', resourceIdSegment);
169
+ }
272
170
  }
273
- });
171
+ }, [resource, rUri, addResource]);
274
172
 
275
- // Generic detection context for all annotation types
276
- // Memoized to keep stable reference
277
- // Note: State setters (setDetectingMotivation, setMotivationDetectionProgress) are stable and don't need deps
278
- const detectionContext = useMemo(() => ({
279
- client,
173
+ // Real-time document events (SSE)
174
+ useResourceEvents({
280
175
  rUri,
281
- setDetectingMotivation,
282
- setMotivationDetectionProgress,
283
- detectionStreamRef,
284
- cacheManager,
285
- showSuccess,
286
- showError
287
- }), [client, rUri, cacheManager, showSuccess, showError]);
288
-
289
- // Generic cancel handler (works for all detection types)
290
- const handleCancelDetection = React.useMemo(
291
- () => createCancelDetectionHandler({
292
- detectionStreamRef,
293
- setDetectingMotivation,
294
- setMotivationDetectionProgress
295
- }),
296
- []
297
- );
176
+ autoConnect: true,
177
+
178
+ // Annotation events - use debounced invalidation to batch rapid updates
179
+ onAnnotationAdded: useCallback((_event: any) => {
180
+ debouncedInvalidateAnnotations();
181
+ }, [debouncedInvalidateAnnotations]),
182
+
183
+ onAnnotationRemoved: useCallback((_event: any) => {
184
+ debouncedInvalidateAnnotations();
185
+ }, [debouncedInvalidateAnnotations]),
186
+
187
+ onAnnotationBodyUpdated: useCallback((event: any) => {
188
+ // Optimistically update annotations cache with body operations
189
+ queryClient.setQueryData(QUERY_KEYS.documents.annotations(rUri), (old: any) => {
190
+ if (!old) return old;
191
+ return {
192
+ ...old,
193
+ annotations: old.annotations.map((annotation: any) => {
194
+ const annotationIdSegment = uriToAnnotationId(annotation.id);
195
+ if (annotationIdSegment === event.payload.annotationId) {
196
+ let bodyArray = Array.isArray(annotation.body) ? [...annotation.body] : [];
197
+
198
+ for (const op of event.payload.operations || []) {
199
+ if (op.op === 'add') {
200
+ bodyArray.push(op.item);
201
+ } else if (op.op === 'remove') {
202
+ bodyArray = bodyArray.filter((item: any) =>
203
+ JSON.stringify(item) !== JSON.stringify(op.item)
204
+ );
205
+ } else if (op.op === 'replace') {
206
+ const index = bodyArray.findIndex((item: any) =>
207
+ JSON.stringify(item) === JSON.stringify(op.oldItem)
208
+ );
209
+ if (index !== -1) {
210
+ bodyArray[index] = op.newItem;
211
+ }
212
+ }
213
+ }
298
214
 
299
- // Handle document generation from stub reference
300
- const handleGenerateDocument = useCallback((
301
- referenceId: string,
302
- options: {
303
- title: string;
304
- prompt?: string;
305
- language?: string;
306
- temperature?: number;
307
- maxTokens?: number;
308
- context?: GenerationContext;
309
- }
310
- ) => {
311
- // Only open modal if this is the initial click (no context provided)
312
- if (!options.context) {
313
- setGenerationReferenceId(referenceId);
314
- setGenerationDefaultTitle(options.title);
315
- setGenerationModalOpen(true);
316
- return;
317
- }
215
+ return {
216
+ ...annotation,
217
+ body: bodyArray,
218
+ };
219
+ }
220
+ return annotation;
221
+ }),
222
+ };
223
+ });
224
+
225
+ queryClient.invalidateQueries({ queryKey: QUERY_KEYS.documents.events(rUri) });
226
+ }, [queryClient, rUri]),
227
+
228
+ // Document status events
229
+ onDocumentArchived: useCallback((_event: any) => {
230
+ refetchDocument();
231
+ showSuccess('This document has been archived');
232
+ debouncedInvalidateAnnotations();
233
+ }, [refetchDocument, showSuccess, debouncedInvalidateAnnotations]),
234
+
235
+ onDocumentUnarchived: useCallback((_event: any) => {
236
+ refetchDocument();
237
+ showSuccess('This document has been unarchived');
238
+ debouncedInvalidateAnnotations();
239
+ }, [refetchDocument, showSuccess, debouncedInvalidateAnnotations]),
240
+
241
+ // Entity tag events
242
+ onEntityTagAdded: useCallback((_event: any) => {
243
+ refetchDocument();
244
+ debouncedInvalidateAnnotations();
245
+ }, [refetchDocument, debouncedInvalidateAnnotations]),
246
+
247
+ onEntityTagRemoved: useCallback((_event: any) => {
248
+ refetchDocument();
249
+ debouncedInvalidateAnnotations();
250
+ }, [refetchDocument, debouncedInvalidateAnnotations]),
251
+
252
+ onError: useCallback((error: any) => {
253
+ console.error('[RealTime] Event stream error:', error);
254
+ }, []),
255
+ });
318
256
 
319
- // Modal submitted with full options including context - proceed with generation
320
- if (!resource) return;
321
-
322
- // Clear CSS sparkle animation if reference was recently created
323
- onClearNewAnnotationId(annotationUri(referenceId));
324
-
325
- // Use full resource URI (W3C Web Annotation spec requires URIs)
326
- const resourceUriStr = resource['@id'];
327
- startGeneration(annotationUri(referenceId), resourceUri(resourceUriStr), {
328
- ...options,
329
- // Use language from modal if provided, otherwise fall back to current locale
330
- language: options.language || locale,
331
- context: options.context
332
- });
333
- }, [startGeneration, resource, onClearNewAnnotationId, locale]);
334
-
335
- // Handle manual document creation from stub reference
336
- const handleCreateDocument = useCallback((
337
- annotationUri: string,
338
- title: string,
339
- entityTypes: string[]
340
- ) => {
341
- if (!resource) return;
342
-
343
- // Extract resource ID from URI
344
- const resourceId = rUri.split('/').pop() || '';
345
-
346
- // Navigate to compose page with reference context
347
- const entityTypesStr = entityTypes.join(',');
348
- const params = new URLSearchParams({
349
- name: title, // Compose page expects 'name' parameter
350
- annotationUri, // Pass full annotation URI, not just ID
351
- sourceDocumentId: resourceId,
352
- ...(entityTypes.length > 0 ? { entityTypes: entityTypesStr } : {}),
353
- });
354
-
355
- window.location.href = `/know/compose?${params.toString()}`;
356
- }, [resource, rUri]);
357
-
358
- // Handle search for documents to link to reference
359
- const handleSearchDocuments = useCallback((referenceId: string, searchTerm: string) => {
360
- setPendingReferenceId(referenceId);
361
- setSearchTerm(searchTerm);
362
- setSearchModalOpen(true);
363
- }, []);
257
+ // Event bus subscriptions (combined into single useEventSubscriptions call to prevent hook ordering issues)
258
+ useEventSubscriptions({
259
+ // Resource operations
260
+ 'resource:archive': async () => {
261
+ try {
262
+ await resources.update.useMutation().mutateAsync({
263
+ rUri,
264
+ data: { archived: true }
265
+ });
266
+ await refetchDocument();
267
+ showSuccess('Document archived');
268
+ } catch (err) {
269
+ console.error('Failed to archive document:', err);
270
+ showError('Failed to archive document');
271
+ }
272
+ },
273
+ 'resource:unarchive': async () => {
274
+ try {
275
+ await resources.update.useMutation().mutateAsync({
276
+ rUri,
277
+ data: { archived: false }
278
+ });
279
+ await refetchDocument();
280
+ showSuccess('Document unarchived');
281
+ } catch (err) {
282
+ console.error('Failed to unarchive document:', err);
283
+ showError('Failed to unarchive document');
284
+ }
285
+ },
286
+ 'resource:clone': async () => {
287
+ try {
288
+ const result = await resources.generateCloneToken.useMutation().mutateAsync(rUri);
289
+ const token = result.token;
290
+ const cloneUrl = `${typeof window !== 'undefined' ? window.location.origin : ''}/know/clone?token=${token}`;
291
+
292
+ await navigator.clipboard.writeText(cloneUrl);
293
+ showSuccess('Clone link copied to clipboard');
294
+ } catch (err) {
295
+ console.error('Failed to generate clone token:', err);
296
+ showError('Failed to generate clone link');
297
+ }
298
+ },
364
299
 
300
+ // Annotation operations
301
+ 'annotation:sparkle': ({ annotationId }) => {
302
+ triggerSparkleAnimation(annotationId);
303
+ },
304
+ 'annotation:created': ({ annotation }) => {
305
+ triggerSparkleAnimation(annotation.id);
306
+ debouncedInvalidateAnnotations();
307
+ },
308
+ 'annotation:deleted': debouncedInvalidateAnnotations,
309
+ 'annotation:create-failed': () => showError('Failed to create annotation'),
310
+ 'annotation:delete-failed': () => showError('Failed to delete annotation'),
311
+ 'annotation:body-updated': () => {
312
+ // Success - optimistic update already applied via useResourceEvents
313
+ },
314
+ 'annotation:body-update-failed': () => showError('Failed to update annotation'),
315
+
316
+ // Settings
317
+ 'settings:theme-changed': ({ theme }) => setTheme(theme),
318
+ 'settings:line-numbers-toggled': toggleLineNumbers,
319
+
320
+ // Detection/Generation
321
+ 'detection:complete': () => showSuccess('Detection complete'),
322
+ 'detection:failed': () => showError('Detection failed'),
323
+ 'generation:complete': () => showSuccess('Document generated'),
324
+ 'generation:failed': () => showError('Failed to generate document'),
325
+
326
+ // Navigation
327
+ 'navigation:reference-navigate': ({ documentId }: { documentId: string }) => {
328
+ // Navigate to the referenced document
329
+ if (routes.resource) {
330
+ const path = routes.resource.replace('[resourceId]', encodeURIComponent(documentId));
331
+ eventBus.emit('navigation:router-push', { path, reason: 'reference-link' });
332
+ }
333
+ },
334
+ 'navigation:entity-type-clicked': ({ entityType }: { entityType: string }) => {
335
+ // Navigate to discovery page filtered by entity type
336
+ if (routes.know) {
337
+ const path = `${routes.know}?entityType=${encodeURIComponent(entityType)}`;
338
+ eventBus.emit('navigation:router-push', { path, reason: 'entity-type-filter' });
339
+ }
340
+ },
341
+ });
342
+
343
+ // Resource loading announcements
344
+ const {
345
+ announceResourceLoading,
346
+ announceResourceLoaded
347
+ } = useResourceLoadingAnnouncements();
365
348
 
366
- // Announce content loading state changes
349
+ // Announce content loading state changes (app-level)
367
350
  useEffect(() => {
368
351
  if (contentLoading) {
369
352
  announceResourceLoading(resource.name);
@@ -372,156 +355,55 @@ export function ResourceViewerPage({
372
355
  }
373
356
  }, [contentLoading, content, resource.name, announceResourceLoading, announceResourceLoaded]);
374
357
 
375
- // Unified annotation request handler - all human-created annotations flow through this
376
- const handleAnnotationRequested = useCallback((pending: PendingAnnotation) => {
377
- // Route to appropriate panel tab based on motivation
378
- const MOTIVATION_TO_TAB: Record<Motivation, string> = {
379
- highlighting: 'annotations',
380
- commenting: 'annotations',
381
- assessing: 'annotations',
382
- tagging: 'annotations',
383
- linking: 'annotations',
384
- bookmarking: 'annotations',
385
- classifying: 'annotations',
386
- describing: 'annotations',
387
- editing: 'annotations',
388
- identifying: 'annotations',
389
- moderating: 'annotations',
390
- questioning: 'annotations',
391
- replying: 'annotations',
392
- };
393
-
394
- setActivePanel(MOTIVATION_TO_TAB[pending.motivation] || 'annotations');
395
- setPendingAnnotation(pending);
396
- }, []);
397
-
398
- // Manual tag creation handler
399
- // Shared UI handlers - same across all annotation types
400
- const handleAnnotationClick = useCallback((annotation: Annotation) => {
401
- setHoveredAnnotationId(annotation.id);
402
- setTimeout(() => setHoveredAnnotationId(null), 1500);
403
- }, []);
404
-
405
- const handleAnnotationHover = useCallback((annotationId: string | null) => {
406
- setHoveredAnnotationId(annotationId);
407
- }, []);
408
-
409
- // Single generic annotation creation handler - reads config from ANNOTATORS
410
- const handleCreateAnnotation = useCallback(async (
411
- motivation: Motivation,
412
- ...args: any[]
413
- ) => {
414
- if (!pendingAnnotation || pendingAnnotation.motivation !== motivation) return;
415
-
416
- // Find the config for this motivation
417
- const annotatorConfig = Object.values(ANNOTATORS).find(a => a.motivation === motivation);
418
- if (!annotatorConfig) return;
419
-
420
- try {
421
- let body: any[] = [];
422
- let selector = pendingAnnotation.selector;
423
-
424
- // Build body based on config
425
- switch (annotatorConfig.create.bodyBuilder) {
426
- case 'empty':
427
- // args[0] might be selector for highlight/assessment
428
- if (args[0]) selector = args[0];
429
- body = [];
430
- break;
431
-
432
- case 'text':
433
- // args[0] is commentText
434
- body = [{
435
- type: 'TextualBody',
436
- value: args[0],
437
- format: 'text/plain',
438
- purpose: 'commenting'
439
- }];
440
- break;
441
-
442
- case 'entityTag':
443
- // args[0] is optional entityType
444
- if (args[0]) {
445
- body = [{
446
- type: 'TextualBody',
447
- purpose: 'tagging',
448
- value: args[0]
449
- }];
450
- }
451
- break;
452
-
453
- case 'dualTag':
454
- // args[0] is schemaId, args[1] is category
455
- body = [
456
- {
457
- type: 'TextualBody',
458
- purpose: 'tagging',
459
- value: args[1] // category
460
- },
461
- {
462
- type: 'TextualBody',
463
- purpose: 'classifying',
464
- value: args[0] // schemaId
465
- }
466
- ];
467
- break;
468
- }
469
-
470
- await onCreateAnnotation(rUri, motivation, selector, body);
471
- setPendingAnnotation(null);
358
+ // Derived state
359
+ const documentEntityTypes = resource.entityTypes || [];
472
360
 
473
- if (annotatorConfig.create.refetchAfter) {
474
- await onRefetchAnnotations();
475
- }
361
+ // Get primary representation metadata
362
+ const primaryRep = getPrimaryRepresentation(resource);
363
+ const primaryMediaType = primaryRep?.mediaType;
364
+ const primaryByteSize = primaryRep?.byteSize;
476
365
 
477
- if (annotatorConfig.create.successMessage) {
478
- const message = annotatorConfig.create.successMessage.replace('{value}', args[1] || '');
479
- showSuccess(message);
480
- }
481
- } catch (error) {
482
- console.error(`Failed to create ${annotatorConfig.internalType}:`, error);
483
- showError(`Failed to create ${annotatorConfig.displayName.toLowerCase()}`);
366
+ // Annotate mode state - local UI state only
367
+ const [annotateMode, _setAnnotateMode] = useState(() => {
368
+ if (typeof window !== 'undefined') {
369
+ return localStorage.getItem('annotateMode') === 'true';
484
370
  }
485
- }, [pendingAnnotation, onCreateAnnotation, rUri, onRefetchAnnotations, showSuccess, showError]);
371
+ return false;
372
+ });
486
373
 
487
374
  // Group annotations by type using static ANNOTATORS
488
- const groups = useMemo(() => {
489
- const result = {
490
- highlights: [] as Annotation[],
491
- references: [] as Annotation[],
492
- assessments: [] as Annotation[],
493
- comments: [] as Annotation[],
494
- tags: [] as Annotation[]
495
- };
496
-
497
- for (const ann of annotations) {
498
- const annotator = Object.values(ANNOTATORS).find(a => a.matchesAnnotation(ann));
499
- if (annotator) {
500
- const key = annotator.internalType + 's'; // highlight -> highlights
501
- if (result[key as keyof typeof result]) {
502
- result[key as keyof typeof result].push(ann);
503
- }
375
+ const result = {
376
+ highlights: [] as Annotation[],
377
+ references: [] as Annotation[],
378
+ assessments: [] as Annotation[],
379
+ comments: [] as Annotation[],
380
+ tags: [] as Annotation[]
381
+ };
382
+
383
+ for (const ann of annotations) {
384
+ const annotator = Object.values(ANNOTATORS).find(a => a.matchesAnnotation(ann));
385
+ if (annotator) {
386
+ const key = annotator.internalType + 's'; // highlight -> highlights
387
+ if (result[key as keyof typeof result]) {
388
+ result[key as keyof typeof result].push(ann);
504
389
  }
505
390
  }
391
+ }
506
392
 
507
- return result;
508
- }, [annotations]);
393
+ const groups = result;
509
394
 
510
- // Memoize resource with content to prevent infinite re-renders
511
- const resourceWithContent = useMemo(
512
- () => ({ ...resource, content }),
513
- [resource, content]
514
- );
395
+ // Combine resource with content
396
+ const resourceWithContent = { ...resource, content };
515
397
 
516
- // Memoize annotation click handlers to prevent infinite re-renders
517
- const handleRefetchAnnotations = useCallback(() => {
518
- // Don't refetch immediately - the SSE event will trigger invalidation after projection is updated
519
- }, []);
398
+ // Handlers for AnnotationHistory (legacy event-based interaction)
399
+ const handleEventHover = useCallback((annotationId: string | null) => {
400
+ if (annotationId) {
401
+ eventBus.emit('annotation:sparkle', { annotationId });
402
+ }
403
+ }, []); // eventBus is stable singleton - never in deps
520
404
 
521
- const handleAnnotationClickAndFocus = useCallback((annotationId: string) => {
522
- setActivePanel('annotations');
523
- setFocusedAnnotationId(annotationId);
524
- setTimeout(() => setFocusedAnnotationId(null), 3000);
405
+ const handleEventClick = useCallback((_annotationId: string | null) => {
406
+ // ResourceViewer now manages scroll state internally
525
407
  }, []);
526
408
 
527
409
  // Document rendering
@@ -566,25 +448,11 @@ export function ResourceViewerPage({
566
448
  ) : (
567
449
  <ResourceViewer
568
450
  resource={resourceWithContent}
569
- annotations={groups}
570
- onRefetchAnnotations={handleRefetchAnnotations}
571
- annotateMode={annotateMode}
572
- onAnnotateModeToggle={handleAnnotateModeToggle}
573
- onAnnotationRequested={handleAnnotationRequested}
574
- onCommentClick={handleAnnotationClickAndFocus}
575
- onReferenceClick={handleAnnotationClickAndFocus}
576
- onHighlightClick={handleAnnotationClickAndFocus}
577
- onAssessmentClick={handleAnnotationClickAndFocus}
578
- onTagClick={handleAnnotationClickAndFocus}
579
- generatingReferenceId={generationProgress?.referenceId ?? null}
580
- onAnnotationHover={setHoveredAnnotationId}
581
- onCommentHover={setHoveredAnnotationId}
582
- hoveredAnnotationId={hoveredAnnotationId}
583
- hoveredCommentId={hoveredAnnotationId}
584
- scrollToAnnotationId={scrollToAnnotationId}
585
- showLineNumbers={showLineNumbers}
586
- annotators={ANNOTATORS}
587
- />
451
+ annotations={groups}
452
+ generatingReferenceId={generationProgress?.referenceId ?? null}
453
+ showLineNumbers={showLineNumbers}
454
+ hoveredAnnotationId={hoveredAnnotationId}
455
+ />
588
456
  )}
589
457
  </ErrorBoundary>
590
458
  </div>
@@ -596,9 +464,7 @@ export function ResourceViewerPage({
596
464
  <ToolbarPanels
597
465
  activePanel={activePanel}
598
466
  theme={theme}
599
- onThemeChange={onThemeChange}
600
467
  showLineNumbers={showLineNumbers}
601
- onLineNumbersToggle={onLineNumbersToggle}
602
468
  width={
603
469
  activePanel === 'jsonld' ? 'w-[600px]' :
604
470
  activePanel === 'annotations' ? 'w-[400px]' :
@@ -619,26 +485,20 @@ export function ResourceViewerPage({
619
485
  <UnifiedAnnotationsPanel
620
486
  annotations={annotations}
621
487
  annotators={ANNOTATORS}
622
- onCreateAnnotation={handleCreateAnnotation}
623
- detectionContext={detectionContext}
624
- focusedAnnotationId={focusedAnnotationId}
625
- hoveredAnnotationId={hoveredAnnotationId}
626
- onAnnotationClick={handleAnnotationClick}
627
- onAnnotationHover={handleAnnotationHover}
628
488
  annotateMode={annotateMode}
629
489
  detectingMotivation={detectingMotivation}
630
- detectionProgress={motivationDetectionProgress}
490
+ detectionProgress={detectionProgress}
631
491
  pendingAnnotation={pendingAnnotation}
632
492
  allEntityTypes={allEntityTypes}
633
- onGenerateDocument={handleGenerateDocument}
634
- onCreateDocument={handleCreateDocument}
635
493
  generatingReferenceId={generationProgress?.referenceId ?? null}
636
- onSearchDocuments={handleSearchDocuments}
637
- onCancelDetection={handleCancelDetection}
638
- {...(primaryMediaType ? { mediaType: primaryMediaType } : {})}
639
494
  referencedBy={referencedBy}
640
495
  referencedByLoading={referencedByLoading}
641
496
  resourceId={rUri.split('/').pop() || ''}
497
+ scrollToAnnotationId={scrollToAnnotationId}
498
+ hoveredAnnotationId={hoveredAnnotationId}
499
+ onScrollCompleted={onScrollCompleted}
500
+ initialTab={panelInitialTab?.tab as any}
501
+ initialTabGeneration={panelInitialTab?.generation}
642
502
  Link={Link}
643
503
  routes={routes}
644
504
  />
@@ -664,9 +524,6 @@ export function ResourceViewerPage({
664
524
  primaryMediaType={primaryMediaType}
665
525
  primaryByteSize={primaryByteSize}
666
526
  isArchived={resource.archived ?? false}
667
- onClone={onClone}
668
- onArchive={onArchive}
669
- onUnarchive={onUnarchive}
670
527
  />
671
528
  )}
672
529
 
@@ -689,7 +546,6 @@ export function ResourceViewerPage({
689
546
  context="document"
690
547
  activePanel={activePanel}
691
548
  isArchived={resource.archived ?? false}
692
- onPanelToggle={onPanelToggle}
693
549
  />
694
550
  </div>
695
551
  </div>
@@ -697,10 +553,7 @@ export function ResourceViewerPage({
697
553
  {/* Search Resources Modal */}
698
554
  <SearchResourcesModal
699
555
  isOpen={searchModalOpen}
700
- onClose={() => {
701
- setSearchModalOpen(false);
702
- setPendingReferenceId(null);
703
- }}
556
+ onClose={onCloseSearchModal}
704
557
  onSelect={async (documentId: string) => {
705
558
  if (pendingReferenceId) {
706
559
  try {
@@ -712,7 +565,8 @@ export function ResourceViewerPage({
712
565
  const resourceIdSegment = rUri.split('/').pop() || '';
713
566
  const nestedUri = `${window.location.origin}/resources/${resourceIdSegment}/annotations/${annotationIdShort}`;
714
567
 
715
- await onUpdateAnnotationBody(resourceAnnotationUri(nestedUri), {
568
+ eventBus.emit('annotation:update-body', {
569
+ annotationUri: resourceAnnotationUri(nestedUri),
716
570
  resourceId: resourceIdSegment,
717
571
  operations: [{
718
572
  op: 'add',
@@ -724,28 +578,23 @@ export function ResourceViewerPage({
724
578
  }],
725
579
  });
726
580
  showSuccess('Reference linked successfully');
727
- await onRefetchAnnotations();
728
- setSearchModalOpen(false);
729
- setPendingReferenceId(null);
581
+ // Cache invalidation now handled by annotation:updated event
582
+ onCloseSearchModal();
730
583
  } catch (error) {
731
584
  console.error('Failed to link reference:', error);
732
585
  showError('Failed to link reference');
733
586
  }
734
587
  }
735
588
  }}
736
- searchTerm={searchTerm}
737
589
  />
738
590
 
739
591
  {/* Generation Config Modal */}
740
592
  <GenerationConfigModal
741
593
  isOpen={generationModalOpen}
742
- onClose={() => {
743
- setGenerationModalOpen(false);
744
- setGenerationReferenceId(null);
745
- }}
594
+ onClose={onCloseGenerationModal}
746
595
  onGenerate={(options: GenerationOptions) => {
747
596
  if (generationReferenceId) {
748
- handleGenerateDocument(generationReferenceId, options);
597
+ onGenerateDocument(generationReferenceId, options);
749
598
  }
750
599
  }}
751
600
  referenceId={generationReferenceId || ''}