@semiont/react-ui 0.4.20 → 0.4.21

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 (108) hide show
  1. package/README.md +8 -5
  2. package/dist/{PdfAnnotationCanvas.client-CHDCGQBR.mjs → PdfAnnotationCanvas.client-6ZGFEN2N.mjs} +9 -13
  3. package/dist/PdfAnnotationCanvas.client-6ZGFEN2N.mjs.map +1 -0
  4. package/dist/TranslationManager-9Xj3MIWQ.d.mts +16 -0
  5. package/dist/chunk-KEDFYI6N.mjs +7788 -0
  6. package/dist/chunk-KEDFYI6N.mjs.map +1 -0
  7. package/dist/index.d.mts +171 -1140
  8. package/dist/index.mjs +3263 -13644
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/test-utils.d.mts +46 -21
  11. package/dist/test-utils.mjs +2499 -87
  12. package/dist/test-utils.mjs.map +1 -1
  13. package/package.json +1 -2
  14. package/src/components/AnnotateReferencesProgressWidget.tsx +21 -28
  15. package/src/components/CodeMirrorRenderer.tsx +9 -11
  16. package/src/components/StatusDisplay.tsx +42 -16
  17. package/src/components/Toolbar.tsx +4 -4
  18. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +34 -20
  19. package/src/components/__tests__/StatusDisplay.test.tsx +47 -64
  20. package/src/components/__tests__/Toolbar.test.tsx +4 -4
  21. package/src/components/annotation/AnnotateToolbar.tsx +8 -7
  22. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +31 -77
  23. package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +0 -1
  24. package/src/components/image-annotation/AnnotationOverlay.tsx +12 -13
  25. package/src/components/image-annotation/SvgDrawingCanvas.tsx +7 -7
  26. package/src/components/modals/PermissionDeniedModal.tsx +11 -11
  27. package/src/components/modals/ReferenceWizardModal.tsx +14 -18
  28. package/src/components/modals/ResourceSearchModal.tsx +10 -6
  29. package/src/components/modals/SearchModal.tsx +10 -6
  30. package/src/components/modals/SessionExpiredModal.tsx +11 -11
  31. package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +7 -7
  32. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +10 -8
  33. package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +10 -7
  34. package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +5 -5
  35. package/src/components/navigation/CollapsibleResourceNavigation.tsx +10 -10
  36. package/src/components/navigation/ObservableLink.tsx +6 -6
  37. package/src/components/navigation/SimpleNavigation.tsx +4 -4
  38. package/src/components/navigation/__tests__/ObservableLink.test.tsx +4 -4
  39. package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +4 -4
  40. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +9 -11
  41. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +0 -1
  42. package/src/components/resource/AnnotateView.tsx +7 -6
  43. package/src/components/resource/AnnotationHistory.tsx +9 -12
  44. package/src/components/resource/BrowseView.tsx +8 -7
  45. package/src/components/resource/ResourceViewer.tsx +17 -25
  46. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +54 -192
  47. package/src/components/resource/__tests__/BrowseView.test.tsx +34 -83
  48. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +40 -31
  49. package/src/components/resource/panels/AssessmentEntry.tsx +5 -4
  50. package/src/components/resource/panels/AssessmentPanel.tsx +19 -15
  51. package/src/components/resource/panels/AssistSection.tsx +11 -13
  52. package/src/components/resource/panels/CollaborationPanel.tsx +29 -7
  53. package/src/components/resource/panels/CommentEntry.tsx +5 -4
  54. package/src/components/resource/panels/CommentsPanel.tsx +9 -11
  55. package/src/components/resource/panels/HighlightEntry.tsx +5 -4
  56. package/src/components/resource/panels/HighlightPanel.tsx +10 -11
  57. package/src/components/resource/panels/ReferenceEntry.tsx +8 -8
  58. package/src/components/resource/panels/ReferencesPanel.tsx +13 -12
  59. package/src/components/resource/panels/ResourceInfoPanel.tsx +7 -6
  60. package/src/components/resource/panels/TagEntry.tsx +5 -4
  61. package/src/components/resource/panels/TaggingPanel.tsx +10 -16
  62. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +3 -2
  63. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +18 -52
  64. package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +51 -20
  65. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +18 -56
  66. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +0 -1
  67. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +4 -5
  68. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +153 -0
  69. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +51 -106
  70. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +15 -47
  71. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +15 -47
  72. package/src/components/settings/SettingsPanel.tsx +8 -8
  73. package/src/components/settings/__tests__/SettingsPanel.test.tsx +12 -12
  74. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +1 -1
  75. package/src/features/admin-exchange/components/AdminExchangePage.tsx +1 -1
  76. package/src/features/admin-exchange/components/ImportCard.tsx +2 -6
  77. package/src/features/admin-security/components/AdminSecurityPage.tsx +1 -1
  78. package/src/features/admin-users/components/AdminUsersPage.tsx +1 -1
  79. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -1
  80. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -1
  81. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -1
  82. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +1 -1
  83. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +5 -3
  84. package/src/features/resource-compose/components/ResourceComposePage.tsx +5 -22
  85. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +4 -3
  86. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +1 -1
  87. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +38 -45
  88. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +123 -192
  89. package/dist/KnowledgeBaseSessionContext-BNNunwzO.d.mts +0 -175
  90. package/dist/PdfAnnotationCanvas.client-CHDCGQBR.mjs.map +0 -1
  91. package/dist/chunk-OZICDVH7.mjs +0 -62
  92. package/dist/chunk-OZICDVH7.mjs.map +0 -1
  93. package/dist/chunk-R4CCMFJH.mjs +0 -877
  94. package/dist/chunk-R4CCMFJH.mjs.map +0 -1
  95. package/dist/chunk-VN5NY4SN.mjs +0 -200
  96. package/dist/chunk-VN5NY4SN.mjs.map +0 -1
  97. package/src/components/modals/ProposeEntitiesModal.tsx +0 -179
  98. package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +0 -129
  99. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +0 -323
  100. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +0 -245
  101. package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +0 -303
  102. package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +0 -150
  103. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +0 -243
  104. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +0 -383
  105. package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +0 -299
  106. package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +0 -186
  107. package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +0 -429
  108. package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +0 -348
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@semiont/react-ui",
3
- "version": "0.4.20",
3
+ "version": "0.4.21",
4
4
  "description": "React components and hooks for Semiont",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.mts",
@@ -73,7 +73,6 @@
73
73
  "test:split:coverage": "npm run test:coverage"
74
74
  },
75
75
  "peerDependencies": {
76
- "@tanstack/react-query": "^5.0.0",
77
76
  "react": "^18.0.0 || ^19.0.0",
78
77
  "react-dom": "^18.0.0 || ^19.0.0"
79
78
  },
@@ -1,19 +1,15 @@
1
1
  'use client';
2
2
 
3
3
  import { useTranslations } from '../contexts/TranslationContext';
4
- import { useEventBus } from '../contexts/EventBusContext';
5
- import type { MarkProgress } from '@semiont/core';
4
+ import { useSemiont } from '../session/SemiontProvider';
5
+ import { useObservable } from '../hooks/useObservable';
6
6
  import type { components } from '@semiont/core';
7
7
 
8
8
  type Motivation = components['schemas']['Motivation'];
9
-
10
- // Extended MarkProgress with optional request parameters
11
- interface EnrichedMarkProgress extends MarkProgress {
12
- requestParams?: Array<{ label: string; value: string }>;
13
- }
9
+ type JobProgress = components['schemas']['JobProgress'];
14
10
 
15
11
  interface AnnotateReferencesProgressWidgetProps {
16
- progress: MarkProgress | null;
12
+ progress: JobProgress | null;
17
13
  annotationType?: Motivation | 'reference';
18
14
  }
19
15
 
@@ -24,11 +20,11 @@ interface AnnotateReferencesProgressWidgetProps {
24
20
  */
25
21
  export function AnnotateReferencesProgressWidget({ progress, annotationType = 'reference' }: AnnotateReferencesProgressWidgetProps) {
26
22
  const t = useTranslations('ReferencesPanel');
27
- const eventBus = useEventBus();
23
+ const session = useObservable(useSemiont().activeSession$);
28
24
 
29
25
  const handleCancel = () => {
30
26
  // Emit event for job cancellation
31
- eventBus.get('job:cancel-requested').next({ jobType: 'annotation' });
27
+ session?.client.emit('job:cancel-requested', { jobType: 'annotation' });
32
28
  };
33
29
 
34
30
  if (!progress) return null;
@@ -36,7 +32,7 @@ export function AnnotateReferencesProgressWidget({ progress, annotationType = 'r
36
32
  return (
37
33
  <div
38
34
  className="semiont-annotation-progress"
39
- data-status={progress.status}
35
+ data-status={progress.stage}
40
36
  data-type={annotationType}
41
37
  >
42
38
  {/* Header with pulsing sparkle */}
@@ -45,7 +41,7 @@ export function AnnotateReferencesProgressWidget({ progress, annotationType = 'r
45
41
  <span className="semiont-annotation-sparkle">✨</span>
46
42
  {t('annotationProgressTitle')}
47
43
  </h3>
48
- {progress.status !== 'complete' && (
44
+ {progress.stage !== 'complete' && (
49
45
  <button
50
46
  onClick={handleCancel}
51
47
  className="semiont-annotation-cancel"
@@ -57,19 +53,16 @@ export function AnnotateReferencesProgressWidget({ progress, annotationType = 'r
57
53
  </div>
58
54
 
59
55
  {/* Request Parameters */}
60
- {(() => {
61
- const enrichedProgress = progress as EnrichedMarkProgress;
62
- return enrichedProgress.requestParams && enrichedProgress.requestParams.length > 0 && (
63
- <div className="semiont-annotation-progress__params">
64
- <div className="semiont-annotation-progress__params-title">Request Parameters:</div>
65
- {enrichedProgress.requestParams.map((param, idx) => (
66
- <div key={idx} className="semiont-annotation-progress__param">
67
- <span className="semiont-annotation-progress__param-label">{param.label}:</span> {param.value}
68
- </div>
69
- ))}
70
- </div>
71
- );
72
- })()}
56
+ {progress.requestParams && progress.requestParams.length > 0 && (
57
+ <div className="semiont-annotation-progress__params">
58
+ <div className="semiont-annotation-progress__params-title">Request Parameters:</div>
59
+ {progress.requestParams.map((param, idx) => (
60
+ <div key={idx} className="semiont-annotation-progress__param">
61
+ <span className="semiont-annotation-progress__param-label">{param.label}:</span> {param.value}
62
+ </div>
63
+ ))}
64
+ </div>
65
+ )}
73
66
 
74
67
  {/* Completed entity types log */}
75
68
  {progress.completedEntityTypes && progress.completedEntityTypes.length > 0 && (
@@ -86,12 +79,12 @@ export function AnnotateReferencesProgressWidget({ progress, annotationType = 'r
86
79
 
87
80
  {/* Status display with pulsing animation */}
88
81
  <div className="semiont-annotation-progress__status">
89
- {progress.status === 'complete' ? (
82
+ {progress.stage === 'complete' ? (
90
83
  <div className="semiont-annotation-progress__message">
91
84
  <span className="semiont-annotation-progress__icon">✅</span>
92
85
  <span>{t('complete')}</span>
93
86
  </div>
94
- ) : progress.status === 'error' ? (
87
+ ) : progress.stage === 'error' ? (
95
88
  <div className="semiont-annotation-progress__message">
96
89
  <span className="semiont-annotation-progress__icon">❌</span>
97
90
  <span>{progress.message || t('failed')}</span>
@@ -102,7 +95,7 @@ export function AnnotateReferencesProgressWidget({ progress, annotationType = 'r
102
95
  <span>{progress.message || (progress.currentEntityType ? t('current', { entityType: progress.currentEntityType }) : t('annotating'))}</span>
103
96
  </div>
104
97
  )}
105
- {progress.currentEntityType && progress.status !== 'complete' && progress.status !== 'error' && (
98
+ {progress.currentEntityType && progress.stage !== 'complete' && progress.stage !== 'error' && (
106
99
  <div className="semiont-annotation-progress__details">
107
100
  Processing: {progress.currentEntityType}
108
101
  </div>
@@ -6,9 +6,7 @@ import { EditorState, RangeSetBuilder, StateField, StateEffect, Compartment } fr
6
6
  import { markdown } from '@codemirror/lang-markdown';
7
7
  import { ReferenceResolutionWidget, showWidgetPreview, hideWidgetPreview } from '../lib/codemirror-widgets';
8
8
  import { scrollAnnotationIntoView } from '../lib/scroll-utils';
9
- import { isReference } from '@semiont/api-client';
10
- import type { EventBus } from "@semiont/core";
11
- import { createHoverHandlers } from '../hooks/useBeckonFlow';
9
+ import { isReference, createHoverHandlers, type SemiontSession } from '@semiont/api-client';
12
10
  import {
13
11
  convertSegmentPositions,
14
12
  computeAnnotationDecorations,
@@ -43,7 +41,7 @@ interface Props {
43
41
  sourceView?: boolean; // If true, show raw source (no markdown rendering)
44
42
  showLineNumbers?: boolean; // If true, show line numbers
45
43
  enableWidgets?: boolean; // If true, show inline widgets (reference previews, entity badges)
46
- eventBus?: EventBus;
44
+ session?: SemiontSession | null | undefined;
47
45
  getTargetResourceName?: (resourceId: string) => string | undefined;
48
46
  generatingReferenceId?: string | null; // ID of reference currently generating a document
49
47
  hoverDelayMs: number; // Hover delay in milliseconds for accessibility
@@ -179,7 +177,7 @@ export function CodeMirrorRenderer({
179
177
  sourceView = false,
180
178
  showLineNumbers = false,
181
179
  enableWidgets = false,
182
- eventBus,
180
+ session,
183
181
  getTargetResourceName,
184
182
  generatingReferenceId,
185
183
  hoverDelayMs
@@ -196,7 +194,7 @@ export function CodeMirrorRenderer({
196
194
  // Index segments by annotation ID for O(1) click lookups
197
195
  const segmentsByIdRef = useRef(new Map<string, TextSegment>());
198
196
  const lineNumbersCompartment = useRef(new Compartment());
199
- const eventBusRef = useRef(eventBus);
197
+ const sessionRef = useRef(session);
200
198
  const getTargetResourceNameRef = useRef(getTargetResourceName);
201
199
 
202
200
  // Update refs when they change
@@ -206,7 +204,7 @@ export function CodeMirrorRenderer({
206
204
  if (s.annotation) segmentsById.set(s.annotation.id, s);
207
205
  }
208
206
  segmentsByIdRef.current = segmentsById;
209
- eventBusRef.current = eventBus;
207
+ sessionRef.current = session;
210
208
  getTargetResourceNameRef.current = getTargetResourceName;
211
209
 
212
210
  // Initialize CodeMirror view once
@@ -238,7 +236,7 @@ export function CodeMirrorRenderer({
238
236
  EditorView.domEventHandlers({
239
237
  click: (event, _view) => {
240
238
  const target = event.target as HTMLElement;
241
- if (eventBusRef.current && handleAnnotationClick(target, segmentsByIdRef.current, eventBusRef.current)) {
239
+ if (sessionRef.current && handleAnnotationClick(target, segmentsByIdRef.current, sessionRef.current)) {
242
240
  event.preventDefault();
243
241
  return true;
244
242
  }
@@ -303,7 +301,7 @@ export function CodeMirrorRenderer({
303
301
  const container = view.dom;
304
302
 
305
303
  const { handleMouseEnter, handleMouseLeave, cleanup: cleanupHover } = createHoverHandlers(
306
- (annotationId) => eventBusRef.current?.get('beckon:hover').next({ annotationId }),
304
+ (annotationId) => sessionRef.current?.client.emit('beckon:hover', { annotationId }),
307
305
  hoverDelayMs
308
306
  );
309
307
 
@@ -329,8 +327,8 @@ export function CodeMirrorRenderer({
329
327
  e.preventDefault();
330
328
  e.stopPropagation();
331
329
 
332
- if (eventBusRef.current) {
333
- dispatchWidgetClick(result, eventBusRef.current);
330
+ if (sessionRef.current) {
331
+ dispatchWidgetClick(result, sessionRef.current);
334
332
  }
335
333
  };
336
334
 
@@ -1,6 +1,8 @@
1
1
  'use client';
2
2
 
3
- import { useHealth } from '../lib/api-hooks';
3
+ import { useEffect, useState } from 'react';
4
+ import { useSemiont } from '../session/SemiontProvider';
5
+ import { useObservable } from '../hooks/useObservable';
4
6
  import './StatusDisplay.css';
5
7
 
6
8
  interface StatusDisplayProps {
@@ -9,36 +11,61 @@ interface StatusDisplayProps {
9
11
  hasValidBackendToken?: boolean;
10
12
  }
11
13
 
14
+ interface StatusData {
15
+ status: string;
16
+ version: string;
17
+ }
18
+
12
19
  export function StatusDisplay({
13
20
  isFullyAuthenticated = false,
14
21
  isAuthenticated = false,
15
22
  hasValidBackendToken = false
16
23
  }: StatusDisplayProps) {
17
- const health = useHealth();
18
- const status = health.status.useQuery(30000); // Poll every 30 seconds
24
+ const semiont = useObservable(useSemiont().activeSession$)?.client;
25
+ const [data, setData] = useState<StatusData | null>(null);
26
+ const [loading, setLoading] = useState(true);
27
+ const [error, setError] = useState<Error | null>(null);
28
+
29
+ useEffect(() => {
30
+ if (!semiont) { setLoading(false); return; }
31
+
32
+ const fetchStatus = () => {
33
+ semiont.getStatus()
34
+ .then((result) => {
35
+ setData(result as StatusData);
36
+ setError(null);
37
+ setLoading(false);
38
+ })
39
+ .catch((err) => {
40
+ setError(err instanceof Error ? err : new Error(String(err)));
41
+ setLoading(false);
42
+ });
43
+ };
44
+
45
+ fetchStatus();
46
+ const interval = setInterval(fetchStatus, 30000);
47
+ return () => clearInterval(interval);
48
+ }, [semiont]);
19
49
 
20
50
  const getStatusContent = () => {
21
- // Check for users who are logged in but missing backend token (old sessions)
22
51
  if (isAuthenticated && !hasValidBackendToken) {
23
52
  return '🚀 Frontend Status: Ready • Backend: Please sign out and sign in again to reconnect';
24
53
  }
25
54
 
26
- // If user is not authenticated at all, show appropriate message
27
55
  if (!isFullyAuthenticated) {
28
56
  return '🚀 Frontend Status: Ready • Backend: Authentication required';
29
57
  }
30
58
 
31
- if (status.data) {
32
- return `🚀 Frontend Status: Ready • Backend: ${status.data.status} (v${status.data.version})`;
59
+ if (data) {
60
+ return `🚀 Frontend Status: Ready • Backend: ${data.status} (v${data.version})`;
33
61
  }
34
62
 
35
- if (status.isLoading) {
63
+ if (loading) {
36
64
  return '🚀 Frontend Status: Ready • Backend: Connecting...';
37
65
  }
38
66
 
39
- if (status.error) {
40
- // Check if this is an auth error that might be fixed by re-login
41
- const errorMessage = status.error instanceof Error ? status.error.message : String(status.error);
67
+ if (error) {
68
+ const errorMessage = error.message;
42
69
  if (errorMessage.includes('401') || errorMessage.includes('Unauthorized')) {
43
70
  return '🚀 Frontend Status: Ready • Backend: Please sign out and sign in again';
44
71
  }
@@ -49,7 +76,6 @@ export function StatusDisplay({
49
76
  };
50
77
 
51
78
  const getStatusType = (): 'warning' | 'info' | 'success' | 'loading' | 'error' => {
52
- // Check for users who need to re-authenticate
53
79
  if (isAuthenticated && !hasValidBackendToken) {
54
80
  return 'warning';
55
81
  }
@@ -58,11 +84,11 @@ export function StatusDisplay({
58
84
  return 'info';
59
85
  }
60
86
 
61
- if (status.data) {
87
+ if (data) {
62
88
  return 'success';
63
89
  }
64
90
 
65
- if (status.isLoading) {
91
+ if (loading) {
66
92
  return 'loading';
67
93
  }
68
94
 
@@ -85,7 +111,7 @@ export function StatusDisplay({
85
111
  <p className="semiont-status-hint">
86
112
  Sign in to view backend status
87
113
  </p>
88
- ) : status.error ? (
114
+ ) : error ? (
89
115
  <p className="semiont-status-hint semiont-status-error-hint" role="alert">
90
116
  <span className="sr-only">Error: </span>
91
117
  Check that the backend server is running and accessible
@@ -93,4 +119,4 @@ export function StatusDisplay({
93
119
  ) : null}
94
120
  </section>
95
121
  );
96
- }
122
+ }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useTranslations } from '../contexts/TranslationContext';
4
- import { useEventBus } from '../contexts/EventBusContext';
4
+ import { useSemiont } from '../session/SemiontProvider';
5
5
  import './toolbar/Toolbar.css';
6
6
 
7
7
  type ToolbarContext = 'document' | 'simple';
@@ -17,7 +17,7 @@ interface Props<T extends string = string> {
17
17
  /**
18
18
  * Toolbar component for panel navigation
19
19
  *
20
- * @emits browse:panel-toggle - Toggle panel visibility. Payload: { panel: string }
20
+ * @emits panel:toggle - Toggle panel visibility. Payload: { panel: string }
21
21
  */
22
22
  export function Toolbar<T extends string = string>({
23
23
  context,
@@ -25,10 +25,10 @@ export function Toolbar<T extends string = string>({
25
25
  isArchived = false
26
26
  }: Props<T>) {
27
27
  const t = useTranslations('Toolbar');
28
- const eventBus = useEventBus();
28
+ const semiont = useSemiont();
29
29
 
30
30
  const handlePanelToggle = (panel: string) => {
31
- eventBus.get('browse:panel-toggle').next({ panel });
31
+ semiont.emit('panel:toggle', { panel });
32
32
  };
33
33
 
34
34
  return (
@@ -4,7 +4,9 @@ import { screen, fireEvent } from '@testing-library/react';
4
4
  import '@testing-library/jest-dom';
5
5
  import { renderWithProviders } from '../../test-utils';
6
6
  import { AnnotateReferencesProgressWidget } from '../AnnotateReferencesProgressWidget';
7
- import type { MarkProgress } from '@semiont/core';
7
+ import type { components } from '@semiont/core';
8
+
9
+ type JobProgress = components['schemas']['JobProgress'];
8
10
 
9
11
  describe('AnnotateReferencesProgressWidget', () => {
10
12
  it('returns null when progress is null', () => {
@@ -15,9 +17,10 @@ describe('AnnotateReferencesProgressWidget', () => {
15
17
  expect(container.firstChild).toBeNull();
16
18
  });
17
19
 
18
- it('renders progress with status message', () => {
19
- const progress: MarkProgress = {
20
- status: 'in-progress',
20
+ it('renders progress with stage message', () => {
21
+ const progress: JobProgress = {
22
+ stage: 'in-progress',
23
+ percentage: 50,
21
24
  message: 'Processing entities...',
22
25
  };
23
26
 
@@ -29,8 +32,9 @@ describe('AnnotateReferencesProgressWidget', () => {
29
32
  });
30
33
 
31
34
  it('shows cancel button when not complete', () => {
32
- const progress: MarkProgress = {
33
- status: 'in-progress',
35
+ const progress: JobProgress = {
36
+ stage: 'in-progress',
37
+ percentage: 30,
34
38
  message: 'Working...',
35
39
  };
36
40
 
@@ -43,8 +47,10 @@ describe('AnnotateReferencesProgressWidget', () => {
43
47
  });
44
48
 
45
49
  it('hides cancel button when complete', () => {
46
- const progress: MarkProgress = {
47
- status: 'complete',
50
+ const progress: JobProgress = {
51
+ stage: 'complete',
52
+ percentage: 100,
53
+ message: '',
48
54
  };
49
55
 
50
56
  renderWithProviders(
@@ -56,8 +62,9 @@ describe('AnnotateReferencesProgressWidget', () => {
56
62
 
57
63
  it('emits job:cancel-requested on cancel click', () => {
58
64
  const handler = vi.fn();
59
- const progress: MarkProgress = {
60
- status: 'in-progress',
65
+ const progress: JobProgress = {
66
+ stage: 'in-progress',
67
+ percentage: 40,
61
68
  message: 'Working...',
62
69
  };
63
70
 
@@ -77,8 +84,10 @@ describe('AnnotateReferencesProgressWidget', () => {
77
84
  });
78
85
 
79
86
  it('renders completed entity types', () => {
80
- const progress: MarkProgress = {
81
- status: 'in-progress',
87
+ const progress: JobProgress = {
88
+ stage: 'in-progress',
89
+ percentage: 60,
90
+ message: '',
82
91
  completedEntityTypes: [
83
92
  { entityType: 'Person', foundCount: 5 },
84
93
  { entityType: 'Organization', foundCount: 3 },
@@ -96,9 +105,11 @@ describe('AnnotateReferencesProgressWidget', () => {
96
105
  expect(foundLabels).toHaveLength(2);
97
106
  });
98
107
 
99
- it('shows complete icon for complete status', () => {
100
- const progress: MarkProgress = {
101
- status: 'complete',
108
+ it('shows complete icon for complete stage', () => {
109
+ const progress: JobProgress = {
110
+ stage: 'complete',
111
+ percentage: 100,
112
+ message: '',
102
113
  };
103
114
 
104
115
  const { container } = renderWithProviders(
@@ -109,9 +120,10 @@ describe('AnnotateReferencesProgressWidget', () => {
109
120
  expect(screen.getByText('ReferencesPanel.complete')).toBeInTheDocument();
110
121
  });
111
122
 
112
- it('shows error message for error status', () => {
113
- const progress: MarkProgress = {
114
- status: 'error',
123
+ it('shows error message for error stage', () => {
124
+ const progress: JobProgress = {
125
+ stage: 'error',
126
+ percentage: 0,
115
127
  message: 'Something went wrong',
116
128
  };
117
129
 
@@ -124,8 +136,10 @@ describe('AnnotateReferencesProgressWidget', () => {
124
136
  });
125
137
 
126
138
  it('shows current entity type processing details', () => {
127
- const progress: MarkProgress = {
128
- status: 'in-progress',
139
+ const progress: JobProgress = {
140
+ stage: 'in-progress',
141
+ percentage: 50,
142
+ message: '',
129
143
  currentEntityType: 'Location',
130
144
  };
131
145
 
@@ -1,42 +1,35 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import React from 'react';
3
- import { screen } from '@testing-library/react';
3
+ import { screen, waitFor } from '@testing-library/react';
4
+ import { BehaviorSubject } from 'rxjs';
4
5
  import '@testing-library/jest-dom';
5
6
  import { renderWithProviders } from '../../test-utils';
6
-
7
- // Mock api-hooks
8
- vi.mock('../../lib/api-hooks', () => ({
9
- useHealth: vi.fn(),
10
- }));
11
-
12
- import { useHealth } from '../../lib/api-hooks';
13
- import type { MockedFunction } from 'vitest';
14
7
  import { StatusDisplay } from '../StatusDisplay';
15
8
 
16
- const mockUseHealth = useHealth as MockedFunction<typeof useHealth>;
9
+ let mockGetStatus: ReturnType<typeof vi.fn>;
10
+ const stableMockClient = {
11
+ get getStatus() { return mockGetStatus; },
12
+ };
13
+ const stableMockSession = { client: stableMockClient };
14
+ const stableActiveSession$ = new BehaviorSubject<any>(stableMockSession);
15
+ const stableMockBrowser = { activeSession$: stableActiveSession$ };
17
16
 
18
- function createMockHealth(queryResult: { data?: unknown; isLoading?: boolean; error?: unknown }) {
17
+ vi.mock('../../session/SemiontProvider', async (importOriginal) => {
18
+ const actual = await importOriginal<typeof import('../../session/SemiontProvider')>();
19
19
  return {
20
- check: {
21
- useQuery: vi.fn(),
22
- },
23
- status: {
24
- useQuery: vi.fn().mockReturnValue({
25
- data: queryResult.data ?? undefined,
26
- isLoading: queryResult.isLoading ?? false,
27
- error: queryResult.error ?? undefined,
28
- }),
29
- },
20
+ ...actual,
21
+ useSemiont: () => stableMockBrowser,
30
22
  };
31
- }
23
+ });
32
24
 
33
25
  describe('StatusDisplay', () => {
34
26
  beforeEach(() => {
35
27
  vi.clearAllMocks();
28
+ mockGetStatus = vi.fn();
36
29
  });
37
30
 
38
31
  it('should show "Authentication required" when not fully authenticated', () => {
39
- mockUseHealth.mockReturnValue(createMockHealth({ data: undefined, isLoading: false }) as ReturnType<typeof useHealth>);
32
+ mockGetStatus.mockResolvedValue({ status: 'healthy', version: '1.0.0' });
40
33
 
41
34
  renderWithProviders(
42
35
  <StatusDisplay isFullyAuthenticated={false} isAuthenticated={false} />
@@ -45,55 +38,45 @@ describe('StatusDisplay', () => {
45
38
  expect(screen.getByText(/Authentication required/)).toBeInTheDocument();
46
39
  });
47
40
 
48
- it('should show "Connecting..." when loading', () => {
49
- mockUseHealth.mockReturnValue(createMockHealth({ isLoading: true }) as ReturnType<typeof useHealth>);
41
+ it('should show status when data is available', async () => {
42
+ mockGetStatus.mockResolvedValue({ status: 'healthy', version: '1.2.3' });
50
43
 
51
44
  renderWithProviders(
52
45
  <StatusDisplay isFullyAuthenticated={true} isAuthenticated={true} hasValidBackendToken={true} />
53
46
  );
54
47
 
55
- expect(screen.getByText(/Connecting\.\.\./)).toBeInTheDocument();
48
+ await waitFor(() => {
49
+ expect(screen.getByText(/healthy/)).toBeInTheDocument();
50
+ expect(screen.getByText(/v1\.2\.3/)).toBeInTheDocument();
51
+ });
56
52
  });
57
53
 
58
- it('should show status when data is available', () => {
59
- mockUseHealth.mockReturnValue(createMockHealth({
60
- data: { status: 'healthy', version: '1.2.3' },
61
- }) as ReturnType<typeof useHealth>);
54
+ it('should show "Connection failed" on error', async () => {
55
+ mockGetStatus.mockRejectedValue(new Error('Network error'));
62
56
 
63
57
  renderWithProviders(
64
58
  <StatusDisplay isFullyAuthenticated={true} isAuthenticated={true} hasValidBackendToken={true} />
65
59
  );
66
60
 
67
- expect(screen.getByText(/healthy/)).toBeInTheDocument();
68
- expect(screen.getByText(/v1\.2\.3/)).toBeInTheDocument();
61
+ await waitFor(() => {
62
+ expect(screen.getByText(/Connection failed/)).toBeInTheDocument();
63
+ });
69
64
  });
70
65
 
71
- it('should show "Connection failed" on error', () => {
72
- mockUseHealth.mockReturnValue(createMockHealth({
73
- error: new Error('Network error'),
74
- }) as ReturnType<typeof useHealth>);
66
+ it('should show re-login message for 401 errors', async () => {
67
+ mockGetStatus.mockRejectedValue(new Error('401 Unauthorized'));
75
68
 
76
69
  renderWithProviders(
77
70
  <StatusDisplay isFullyAuthenticated={true} isAuthenticated={true} hasValidBackendToken={true} />
78
71
  );
79
72
 
80
- expect(screen.getByText(/Connection failed/)).toBeInTheDocument();
81
- });
82
-
83
- it('should show re-login message for 401 errors', () => {
84
- mockUseHealth.mockReturnValue(createMockHealth({
85
- error: new Error('401 Unauthorized'),
86
- }) as ReturnType<typeof useHealth>);
87
-
88
- renderWithProviders(
89
- <StatusDisplay isFullyAuthenticated={true} isAuthenticated={true} hasValidBackendToken={true} />
90
- );
91
-
92
- expect(screen.getByText(/sign out and sign in again/i)).toBeInTheDocument();
73
+ await waitFor(() => {
74
+ expect(screen.getByText(/sign out and sign in again/i)).toBeInTheDocument();
75
+ });
93
76
  });
94
77
 
95
78
  it('should show warning for authenticated users missing backend token', () => {
96
- mockUseHealth.mockReturnValue(createMockHealth({ data: undefined }) as ReturnType<typeof useHealth>);
79
+ mockGetStatus.mockResolvedValue(undefined);
97
80
 
98
81
  renderWithProviders(
99
82
  <StatusDisplay isFullyAuthenticated={false} isAuthenticated={true} hasValidBackendToken={false} />
@@ -103,7 +86,7 @@ describe('StatusDisplay', () => {
103
86
  });
104
87
 
105
88
  it('should have role="status"', () => {
106
- mockUseHealth.mockReturnValue(createMockHealth({ data: undefined }) as ReturnType<typeof useHealth>);
89
+ mockGetStatus.mockResolvedValue(undefined);
107
90
 
108
91
  renderWithProviders(
109
92
  <StatusDisplay isFullyAuthenticated={false} isAuthenticated={false} />
@@ -113,7 +96,7 @@ describe('StatusDisplay', () => {
113
96
  });
114
97
 
115
98
  it('should have aria-live="polite"', () => {
116
- mockUseHealth.mockReturnValue(createMockHealth({ data: undefined }) as ReturnType<typeof useHealth>);
99
+ mockGetStatus.mockResolvedValue(undefined);
117
100
 
118
101
  renderWithProviders(
119
102
  <StatusDisplay isFullyAuthenticated={false} isAuthenticated={false} />
@@ -124,7 +107,7 @@ describe('StatusDisplay', () => {
124
107
  });
125
108
 
126
109
  it('should show sign-in hint when not authenticated', () => {
127
- mockUseHealth.mockReturnValue(createMockHealth({ data: undefined }) as ReturnType<typeof useHealth>);
110
+ mockGetStatus.mockResolvedValue(undefined);
128
111
 
129
112
  renderWithProviders(
130
113
  <StatusDisplay isFullyAuthenticated={false} isAuthenticated={false} />
@@ -133,28 +116,28 @@ describe('StatusDisplay', () => {
133
116
  expect(screen.getByText('Sign in to view backend status')).toBeInTheDocument();
134
117
  });
135
118
 
136
- it('should show error hint when there is an error', () => {
137
- mockUseHealth.mockReturnValue(createMockHealth({
138
- error: new Error('Connection refused'),
139
- }) as ReturnType<typeof useHealth>);
119
+ it('should show error hint when there is an error', async () => {
120
+ mockGetStatus.mockRejectedValue(new Error('Connection refused'));
140
121
 
141
122
  renderWithProviders(
142
123
  <StatusDisplay isFullyAuthenticated={true} isAuthenticated={true} hasValidBackendToken={true} />
143
124
  );
144
125
 
145
- expect(screen.getByText(/Check that the backend server is running/)).toBeInTheDocument();
126
+ await waitFor(() => {
127
+ expect(screen.getByText(/Check that the backend server is running/)).toBeInTheDocument();
128
+ });
146
129
  });
147
130
 
148
- it('should set data-status attribute based on state', () => {
149
- mockUseHealth.mockReturnValue(createMockHealth({
150
- data: { status: 'healthy', version: '1.0.0' },
151
- }) as ReturnType<typeof useHealth>);
131
+ it('should set data-status attribute based on state', async () => {
132
+ mockGetStatus.mockResolvedValue({ status: 'healthy', version: '1.0.0' });
152
133
 
153
134
  renderWithProviders(
154
135
  <StatusDisplay isFullyAuthenticated={true} isAuthenticated={true} hasValidBackendToken={true} />
155
136
  );
156
137
 
157
- const status = screen.getByRole('status');
158
- expect(status).toHaveAttribute('data-status', 'success');
138
+ await waitFor(() => {
139
+ const status = screen.getByRole('status');
140
+ expect(status).toHaveAttribute('data-status', 'success');
141
+ });
159
142
  });
160
143
  });