@semiont/react-ui 0.2.33 → 0.2.34-build.89

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 (82) hide show
  1. package/dist/EventBusContext-BmzEcGHZ.d.mts +177 -0
  2. package/dist/{PdfAnnotationCanvas.client-FGV33CWN.mjs → PdfAnnotationCanvas.client-VLNA5O5M.mjs} +7 -7
  3. package/dist/PdfAnnotationCanvas.client-VLNA5O5M.mjs.map +1 -0
  4. package/dist/{chunk-YPYLOBA2.mjs → chunk-C63BARI7.mjs} +3 -2
  5. package/dist/chunk-C63BARI7.mjs.map +1 -0
  6. package/dist/{chunk-FC6SGLLT.mjs → chunk-M7SZRRIE.mjs} +24 -16
  7. package/dist/chunk-M7SZRRIE.mjs.map +1 -0
  8. package/dist/chunk-ULIET3MW.mjs +31 -0
  9. package/dist/chunk-ULIET3MW.mjs.map +1 -0
  10. package/dist/index.d.mts +33 -60
  11. package/dist/index.mjs +171 -363
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/test-utils.d.mts +3 -5
  14. package/dist/test-utils.mjs +2 -2
  15. package/dist/test-utils.mjs.map +1 -1
  16. package/package.json +2 -3
  17. package/src/components/CodeMirrorRenderer.tsx +4 -4
  18. package/src/components/DetectionProgressWidget.tsx +3 -3
  19. package/src/components/LiveRegion.tsx +1 -1
  20. package/src/components/Toolbar.tsx +1 -1
  21. package/src/components/annotation/AnnotateToolbar.tsx +5 -5
  22. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +4 -4
  23. package/src/components/annotation-popups/JsonLdView.tsx +1 -1
  24. package/src/components/image-annotation/AnnotationOverlay.tsx +9 -9
  25. package/src/components/image-annotation/SvgDrawingCanvas.tsx +5 -5
  26. package/src/components/navigation/CollapsibleResourceNavigation.tsx +4 -4
  27. package/src/components/navigation/ObservableLink.tsx +1 -1
  28. package/src/components/navigation/SimpleNavigation.tsx +1 -1
  29. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +6 -6
  30. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +2 -2
  31. package/src/components/resource/AnnotateView.tsx +5 -4
  32. package/src/components/resource/AnnotationHistory.tsx +1 -1
  33. package/src/components/resource/BrowseView.tsx +5 -4
  34. package/src/components/resource/ResourceViewer.tsx +5 -4
  35. package/src/components/resource/__tests__/BrowseView.test.tsx +11 -22
  36. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +1 -1
  37. package/src/components/resource/event-formatting.ts +1 -1
  38. package/src/components/resource/panels/AssessmentEntry.tsx +2 -2
  39. package/src/components/resource/panels/AssessmentPanel.tsx +4 -4
  40. package/src/components/resource/panels/CommentEntry.tsx +2 -2
  41. package/src/components/resource/panels/CommentsPanel.tsx +4 -4
  42. package/src/components/resource/panels/DetectSection.tsx +3 -3
  43. package/src/components/resource/panels/HighlightEntry.tsx +2 -2
  44. package/src/components/resource/panels/HighlightPanel.tsx +2 -2
  45. package/src/components/resource/panels/JsonLdPanel.tsx +1 -1
  46. package/src/components/resource/panels/ReferenceEntry.tsx +6 -6
  47. package/src/components/resource/panels/ReferencesPanel.tsx +5 -5
  48. package/src/components/resource/panels/ResourceInfoPanel.tsx +3 -3
  49. package/src/components/resource/panels/StatisticsPanel.tsx +1 -1
  50. package/src/components/resource/panels/TagEntry.tsx +2 -2
  51. package/src/components/resource/panels/TaggingPanel.tsx +5 -5
  52. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +1 -1
  53. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +5 -5
  54. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +10 -10
  55. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +5 -5
  56. package/src/components/resource/panels/__tests__/DetectSection.test.tsx +9 -9
  57. package/src/components/resource/panels/__tests__/HighlightPanel.detectionProgress.test.tsx +1 -1
  58. package/src/components/resource/panels/__tests__/JsonLdPanel.test.tsx +1 -1
  59. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +4 -4
  60. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +4 -4
  61. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +5 -5
  62. package/src/components/settings/SettingsPanel.tsx +5 -5
  63. package/src/components/viewers/ImageViewer.tsx +1 -1
  64. package/src/features/resource-compose/components/ResourceComposePage.tsx +1 -1
  65. package/src/features/resource-discovery/components/ResourceCard.tsx +1 -1
  66. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +1 -1
  67. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +7 -5
  68. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +5 -4
  69. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +5 -5
  70. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +29 -43
  71. package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +20 -39
  72. package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +38 -46
  73. package/src/features/resource-viewer/__tests__/ResolutionFlowIntegration.test.tsx +36 -43
  74. package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +8 -8
  75. package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +14 -21
  76. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +8 -7
  77. package/dist/EventBusContext-CJjL_cCf.d.mts +0 -462
  78. package/dist/PdfAnnotationCanvas.client-FGV33CWN.mjs.map +0 -1
  79. package/dist/chunk-FC6SGLLT.mjs.map +0 -1
  80. package/dist/chunk-XS27QKGP.mjs +0 -55
  81. package/dist/chunk-XS27QKGP.mjs.map +0 -1
  82. package/dist/chunk-YPYLOBA2.mjs.map +0 -1
@@ -6,7 +6,7 @@ import userEvent from '@testing-library/user-event';
6
6
  import '@testing-library/jest-dom';
7
7
  import { TaggingPanel } from '../TaggingPanel';
8
8
  import { EventBusProvider, resetEventBusForTesting, useEventBus } from '../../../../contexts/EventBusContext';
9
- import type { components } from '@semiont/api-client';
9
+ import type { components } from '@semiont/core';
10
10
 
11
11
  type Annotation = components['schemas']['Annotation'];
12
12
 
@@ -29,16 +29,16 @@ function createEventTracker() {
29
29
  events.push({ event: eventName, payload });
30
30
  };
31
31
 
32
- const panelEvents = ['annotation:create', 'detection:start'];
32
+ const panelEvents = ['annotation:create', 'detection:start'] as const;
33
33
 
34
34
  panelEvents.forEach(eventName => {
35
35
  const handler = trackEvent(eventName);
36
- eventBus.on(eventName, handler);
37
- handlers.push(() => eventBus.off(eventName, handler));
36
+ const subscription = eventBus.get(eventName).subscribe(handler);
37
+ handlers.push(subscription);
38
38
  });
39
39
 
40
40
  return () => {
41
- handlers.forEach(cleanup => cleanup());
41
+ handlers.forEach(sub => sub.unsubscribe());
42
42
  };
43
43
  }, [eventBus]);
44
44
 
@@ -38,7 +38,7 @@ export function SettingsPanel({
38
38
  const handleLocaleChange = (newLocale: string) => {
39
39
  const localeName = LOCALES.find(l => l.code === newLocale)?.nativeName || newLocale;
40
40
  announceLanguageChanging(localeName);
41
- eventBus.emit('settings:locale-changed', { locale: newLocale });
41
+ eventBus.get('settings:locale-changed').next({ locale: newLocale });
42
42
  };
43
43
 
44
44
  // Announce when language has successfully changed
@@ -67,7 +67,7 @@ export function SettingsPanel({
67
67
  type="button"
68
68
  role="switch"
69
69
  aria-checked={showLineNumbers}
70
- onClick={() => eventBus.emit('settings:line-numbers-toggled', undefined)}
70
+ onClick={() => eventBus.get('settings:line-numbers-toggled').next(undefined)}
71
71
  className={`semiont-toggle ${
72
72
  showLineNumbers ? 'semiont-toggle--active' : ''
73
73
  }`}
@@ -91,7 +91,7 @@ export function SettingsPanel({
91
91
  </label>
92
92
  <div className="semiont-settings-panel__button-group">
93
93
  <button
94
- onClick={() => eventBus.emit('settings:theme-changed', { theme: 'light' })}
94
+ onClick={() => eventBus.get('settings:theme-changed').next({ theme: 'light' })}
95
95
  className={`semiont-panel-button ${
96
96
  theme === 'light' ? 'semiont-panel-button-active' : ''
97
97
  }`}
@@ -100,7 +100,7 @@ export function SettingsPanel({
100
100
  ☀️ {t('themeLight')}
101
101
  </button>
102
102
  <button
103
- onClick={() => eventBus.emit('settings:theme-changed', { theme: 'dark' })}
103
+ onClick={() => eventBus.get('settings:theme-changed').next({ theme: 'dark' })}
104
104
  className={`semiont-panel-button ${
105
105
  theme === 'dark' ? 'semiont-panel-button-active' : ''
106
106
  }`}
@@ -109,7 +109,7 @@ export function SettingsPanel({
109
109
  🌙 {t('themeDark')}
110
110
  </button>
111
111
  <button
112
- onClick={() => eventBus.emit('settings:theme-changed', { theme: 'system' })}
112
+ onClick={() => eventBus.get('settings:theme-changed').next({ theme: 'system' })}
113
113
  className={`semiont-panel-button ${
114
114
  theme === 'system' ? 'semiont-panel-button-active' : ''
115
115
  }`}
@@ -1,4 +1,4 @@
1
- import type { ResourceUri } from '@semiont/api-client';
1
+ import type { ResourceUri } from '@semiont/core';
2
2
 
3
3
  interface ImageViewerProps {
4
4
  resourceUri: ResourceUri;
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import React, { useState, useEffect } from 'react';
10
- import type { components } from '@semiont/api-client';
10
+ import type { components } from '@semiont/core';
11
11
  import { isImageMimeType, isPdfMimeType, LOCALES } from '@semiont/api-client';
12
12
  import { buttonStyles } from '../../../lib/button-styles';
13
13
  import { CodeMirrorRenderer } from '../../../components/CodeMirrorRenderer';
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import React from 'react';
9
- import type { components } from '@semiont/api-client';
9
+ import type { components } from '@semiont/core';
10
10
 
11
11
  type ResourceDescriptor = components['schemas']['ResourceDescriptor'];
12
12
 
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import React, { useState, useCallback, useRef } from 'react';
9
- import type { components } from '@semiont/api-client';
9
+ import type { components } from '@semiont/core';
10
10
  import { getResourceId } from '@semiont/api-client';
11
11
  import { useRovingTabIndex } from '../../../hooks/useRovingTabIndex';
12
12
  import { Toolbar } from '../../../components/Toolbar';
@@ -25,10 +25,10 @@ import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../..
25
25
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
26
26
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
27
27
  import { SemiontApiClient } from '@semiont/api-client';
28
- import { resourceUri } from '@semiont/api-client';
28
+ import { resourceUri } from '@semiont/core';
29
29
  import type { Emitter } from 'mitt';
30
- import type { EventMap } from '../../../contexts/EventBusContext';
31
- import type { Motivation, Selector } from '@semiont/api-client';
30
+ import type { EventMap } from '@semiont/core';
31
+ import type { Motivation, Selector } from '@semiont/core';
32
32
 
33
33
  const TEST_URI = resourceUri('http://localhost:4000/resources/test-resource');
34
34
 
@@ -85,7 +85,7 @@ function renderDetectionFlow(testUri: string) {
85
85
  return {
86
86
  getEventBus: () => eventBusInstance,
87
87
  emit: <K extends keyof EventMap>(event: K, payload: EventMap[K]) => {
88
- eventBusInstance.emit(event, payload);
88
+ eventBusInstance.get(event).next(payload);
89
89
  },
90
90
  };
91
91
  }
@@ -239,7 +239,7 @@ describe('Annotation creation clears pendingAnnotation', () => {
239
239
  const createdListener = vi.fn();
240
240
  // Set listener after first render so eventBus is captured
241
241
  await waitFor(() => expect(getEventBus()).toBeDefined());
242
- getEventBus().on('annotation:created', createdListener);
242
+ const subscription = getEventBus().get('annotation:created').subscribe(createdListener);
243
243
 
244
244
  act(() => {
245
245
  emit('annotation:requested', { selector: TEXT_SELECTOR, motivation: 'linking' });
@@ -257,6 +257,8 @@ describe('Annotation creation clears pendingAnnotation', () => {
257
257
  expect(createdListener).toHaveBeenCalledTimes(1);
258
258
  expect(createdListener).toHaveBeenCalledWith({ annotation: MOCK_ANNOTATION });
259
259
  });
260
+
261
+ subscription.unsubscribe();
260
262
  });
261
263
 
262
264
  it('does NOT clear pendingAnnotation if API call fails', async () => {
@@ -30,7 +30,8 @@ import { useDetectionFlow } from '../../../hooks/useDetectionFlow';
30
30
  import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../../contexts/EventBusContext';
31
31
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
32
32
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
33
- import { SemiontApiClient, resourceUri, accessToken } from '@semiont/api-client';
33
+ import { SemiontApiClient } from '@semiont/api-client';
34
+ import { resourceUri, accessToken } from '@semiont/core';
34
35
 
35
36
  describe('Annotation Deletion - Feature Integration', () => {
36
37
  let deleteAnnotationSpy: ReturnType<typeof vi.fn>;
@@ -78,7 +79,7 @@ describe('Annotation Deletion - Feature Integration', () => {
78
79
  return {
79
80
  emitDelete: (annotationId: string) => {
80
81
  act(() => {
81
- eventBusInstance!.emit('annotation:delete', { annotationId });
82
+ eventBusInstance!.get('annotation:delete').next({ annotationId });
82
83
  });
83
84
  },
84
85
  eventBus: eventBusInstance!,
@@ -127,7 +128,7 @@ describe('Annotation Deletion - Feature Integration', () => {
127
128
  const deletedListener = vi.fn();
128
129
 
129
130
  // Subscribe to success event
130
- eventBus.on('annotation:deleted', deletedListener);
131
+ eventBus.get('annotation:deleted').subscribe(deletedListener);
131
132
 
132
133
  emitDelete('annotation-789');
133
134
 
@@ -152,7 +153,7 @@ describe('Annotation Deletion - Feature Integration', () => {
152
153
  const failedListener = vi.fn();
153
154
 
154
155
  // Subscribe to failure event
155
- eventBus.on('annotation:delete-failed', failedListener);
156
+ eventBus.get('annotation:delete-failed').subscribe(failedListener);
156
157
 
157
158
  emitDelete('annotation-error');
158
159
 
@@ -76,7 +76,7 @@ describe('REPRODUCING BUG: Detection state not updating', () => {
76
76
 
77
77
  // Emit detection:start event (exactly like production)
78
78
  act(() => {
79
- eventBusInstance.emit('detection:start', {
79
+ eventBusInstance.get('detection:start').next({
80
80
  motivation: 'linking',
81
81
  options: { entityTypes: ['Location'] }
82
82
  });
@@ -124,7 +124,7 @@ describe('REPRODUCING BUG: Detection state not updating', () => {
124
124
 
125
125
  // Emit detection:progress event (exactly like production)
126
126
  act(() => {
127
- eventBusInstance.emit('detection:progress', {
127
+ eventBusInstance.get('detection:progress').next({
128
128
  status: 'started',
129
129
  resourceId: 'test',
130
130
  totalEntityTypes: 1,
@@ -183,7 +183,7 @@ describe('REPRODUCING BUG: Detection state not updating', () => {
183
183
  // Exactly like production logs
184
184
  act(() => {
185
185
  console.log('[EventBus] emit: detection:start {motivation: "linking", options: {...}}');
186
- eventBusInstance.emit('detection:start', {
186
+ eventBusInstance.get('detection:start').next({
187
187
  motivation: 'linking',
188
188
  options: { entityTypes: ['Location'] }
189
189
  });
@@ -193,7 +193,7 @@ describe('REPRODUCING BUG: Detection state not updating', () => {
193
193
 
194
194
  act(() => {
195
195
  console.log('[EventBus] emit: detection:progress {status: "started", ...}');
196
- eventBusInstance.emit('detection:progress', {
196
+ eventBusInstance.get('detection:progress').next({
197
197
  status: 'started',
198
198
  resourceId: 'f45fd44f9cb0b0fe1b7980d3d034bc61',
199
199
  totalEntityTypes: 1,
@@ -206,7 +206,7 @@ describe('REPRODUCING BUG: Detection state not updating', () => {
206
206
 
207
207
  act(() => {
208
208
  console.log('[EventBus] emit: detection:progress {status: "scanning", ...}');
209
- eventBusInstance.emit('detection:progress', {
209
+ eventBusInstance.get('detection:progress').next({
210
210
  status: 'scanning',
211
211
  resourceId: 'f45fd44f9cb0b0fe1b7980d3d034bc61',
212
212
  currentEntityType: 'Location',
@@ -30,32 +30,16 @@ import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../..
30
30
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
31
31
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
32
32
  import { SSEClient } from '@semiont/api-client';
33
- import type { Motivation } from '@semiont/api-client';
34
- import { resourceUri } from '@semiont/api-client';
33
+ import type { Motivation } from '@semiont/core';
34
+ import { resourceUri } from '@semiont/core';
35
35
  import type { Emitter } from 'mitt';
36
- import type { EventMap } from '../../../contexts/EventBusContext';
36
+ import type { EventMap } from '@semiont/core';
37
37
 
38
- // Mock SSE stream that we can control in tests
38
+ // Mock SSE stream - SSE now emits directly to EventBus, no callbacks
39
39
  const createMockSSEStream = () => {
40
- const stream = {
41
- onProgressCallback: null as ((chunk: any) => void) | null,
42
- onCompleteCallback: null as (() => void) | null,
43
- onErrorCallback: null as ((error: Error) => void) | null,
44
- onProgress: vi.fn((callback: (chunk: any) => void) => {
45
- stream.onProgressCallback = callback;
46
- return stream;
47
- }),
48
- onComplete: vi.fn((callback: () => void) => {
49
- stream.onCompleteCallback = callback;
50
- return stream;
51
- }),
52
- onError: vi.fn((callback: (error: Error) => void) => {
53
- stream.onErrorCallback = callback;
54
- return stream;
55
- }),
40
+ return {
56
41
  close: vi.fn(),
57
42
  };
58
- return stream;
59
43
  };
60
44
 
61
45
  describe('Detection Flow - Feature Integration', () => {
@@ -102,14 +86,14 @@ describe('Detection Flow - Feature Integration', () => {
102
86
  expect(detectReferencesSpy).toHaveBeenCalledTimes(1);
103
87
  });
104
88
 
105
- // Verify correct parameters
89
+ // Verify correct parameters (eventBus is passed but we don't need to verify its exact value)
106
90
  expect(detectReferencesSpy).toHaveBeenCalledWith(
107
91
  testUri,
108
92
  {
109
93
  entityTypes: ['Person', 'Organization'],
110
94
  includeDescriptiveReferences: false,
111
95
  },
112
- { auth: undefined }
96
+ expect.objectContaining({ auth: undefined })
113
97
  );
114
98
  });
115
99
 
@@ -117,7 +101,7 @@ describe('Detection Flow - Feature Integration', () => {
117
101
  const testUri = resourceUri('http://localhost:4000/resources/test-resource');
118
102
 
119
103
  // Render with state observer
120
- const { emitDetectionStart } = renderDetectionFlow(testUri);
104
+ const { emitDetectionStart, getEventBus } = renderDetectionFlow(testUri);
121
105
 
122
106
  // Start detection
123
107
  act(() => {
@@ -131,9 +115,9 @@ describe('Detection Flow - Feature Integration', () => {
131
115
  expect(detectReferencesSpy).toHaveBeenCalled();
132
116
  });
133
117
 
134
- // Simulate SSE progress callback being invoked
118
+ // Simulate SSE progress event being emitted to EventBus (how SSE actually works now)
135
119
  act(() => {
136
- mockStream.onProgressCallback!({
120
+ getEventBus().get('detection:progress').next({
137
121
  status: 'scanning',
138
122
  message: 'Scanning for Person...',
139
123
  currentEntityType: 'Person',
@@ -152,7 +136,7 @@ describe('Detection Flow - Feature Integration', () => {
152
136
 
153
137
  it('should handle multiple progress updates correctly', async () => {
154
138
  const testUri = resourceUri('http://localhost:4000/resources/test-resource');
155
- const { emitDetectionStart } = renderDetectionFlow(testUri);
139
+ const { emitDetectionStart, getEventBus } = renderDetectionFlow(testUri);
156
140
 
157
141
  // Start detection
158
142
  act(() => {
@@ -165,9 +149,9 @@ describe('Detection Flow - Feature Integration', () => {
165
149
  expect(detectHighlightsSpy).toHaveBeenCalledTimes(1);
166
150
  });
167
151
 
168
- // First progress update
152
+ // First progress update via EventBus
169
153
  act(() => {
170
- mockStream.onProgressCallback!({
154
+ getEventBus().get('detection:progress').next({
171
155
  status: 'started',
172
156
  message: 'Starting analysis...',
173
157
  percentage: 0,
@@ -178,9 +162,9 @@ describe('Detection Flow - Feature Integration', () => {
178
162
  expect(screen.getByTestId('progress')).toHaveTextContent('Starting analysis...');
179
163
  });
180
164
 
181
- // Second progress update
165
+ // Second progress update via EventBus
182
166
  act(() => {
183
- mockStream.onProgressCallback!({
167
+ getEventBus().get('detection:progress').next({
184
168
  status: 'analyzing',
185
169
  message: 'Analyzing text...',
186
170
  percentage: 50,
@@ -191,9 +175,9 @@ describe('Detection Flow - Feature Integration', () => {
191
175
  expect(screen.getByTestId('progress')).toHaveTextContent('Analyzing text...');
192
176
  });
193
177
 
194
- // Final progress update
178
+ // Final progress update via EventBus
195
179
  act(() => {
196
- mockStream.onProgressCallback!({
180
+ getEventBus().get('detection:progress').next({
197
181
  status: 'complete',
198
182
  message: 'Created 14 highlights',
199
183
  percentage: 100,
@@ -218,9 +202,9 @@ describe('Detection Flow - Feature Integration', () => {
218
202
  expect(screen.getByTestId('detecting')).toHaveTextContent('highlighting');
219
203
  });
220
204
 
221
- // Send final progress
205
+ // Send final progress via EventBus
222
206
  act(() => {
223
- mockStream.onProgressCallback!({
207
+ getEventBus().get('detection:progress').next({
224
208
  status: 'complete',
225
209
  message: 'Created 14 highlights',
226
210
  });
@@ -232,7 +216,7 @@ describe('Detection Flow - Feature Integration', () => {
232
216
 
233
217
  // Emit completion event
234
218
  act(() => {
235
- getEventBus().emit('detection:complete', { motivation: 'highlighting' });
219
+ getEventBus().get('detection:complete').next({ motivation: 'highlighting' });
236
220
  });
237
221
 
238
222
  // Verify: detecting flag cleared BUT progress still visible
@@ -251,9 +235,9 @@ describe('Detection Flow - Feature Integration', () => {
251
235
  emitDetectionStart('linking', { entityTypes: ['Person'] });
252
236
  });
253
237
 
254
- // Add some progress
238
+ // Add some progress via EventBus
255
239
  act(() => {
256
- mockStream.onProgressCallback!({
240
+ getEventBus().get('detection:progress').next({
257
241
  status: 'scanning',
258
242
  message: 'Scanning...',
259
243
  });
@@ -265,7 +249,7 @@ describe('Detection Flow - Feature Integration', () => {
265
249
 
266
250
  // Emit failure
267
251
  act(() => {
268
- getEventBus().emit('detection:failed', { type: 'job.failed', resourceId: 'test-resource' as any, payload: { jobId: 'job-1' as any, jobType: 'detection', error: 'Network error' } });
252
+ getEventBus().get('detection:failed').next({ type: 'job.failed', resourceId: 'test-resource' as any, payload: { jobId: 'job-1' as any, jobType: 'detection', error: 'Network error' } });
269
253
  });
270
254
 
271
255
  // Verify: both detecting and progress cleared
@@ -288,7 +272,7 @@ describe('Detection Flow - Feature Integration', () => {
288
272
  expect(detectHighlightsSpy).toHaveBeenCalledTimes(1);
289
273
  expect(detectHighlightsSpy).toHaveBeenCalledWith(testUri, {
290
274
  instructions: 'Find important text',
291
- }, { auth: undefined });
275
+ }, expect.objectContaining({ auth: undefined }));
292
276
  });
293
277
 
294
278
  // Reset for next test
@@ -309,7 +293,7 @@ describe('Detection Flow - Feature Integration', () => {
309
293
  expect(detectCommentsSpy).toHaveBeenCalledWith(testUri, {
310
294
  instructions: 'Add helpful comments',
311
295
  tone: 'educational',
312
- }, { auth: undefined });
296
+ }, expect.objectContaining({ auth: undefined }));
313
297
  });
314
298
  });
315
299
 
@@ -322,7 +306,7 @@ describe('Detection Flow - Feature Integration', () => {
322
306
 
323
307
  // Add an additional event listener (simulating multiple subscribers)
324
308
  const additionalListener = vi.fn();
325
- getEventBus().on('detection:start', additionalListener);
309
+ const subscription = getEventBus().get('detection:start').subscribe(additionalListener);
326
310
 
327
311
  // Trigger detection
328
312
  act(() => {
@@ -339,6 +323,8 @@ describe('Detection Flow - Feature Integration', () => {
339
323
 
340
324
  // VERIFY: Our additional listener was called (events work)
341
325
  expect(additionalListener).toHaveBeenCalledTimes(1);
326
+
327
+ subscription.unsubscribe();
342
328
  });
343
329
  });
344
330
 
@@ -381,7 +367,7 @@ function renderDetectionFlow(testUri: string) {
381
367
 
382
368
  return {
383
369
  emitDetectionStart: (motivation: Motivation, options: any) => {
384
- eventBusInstance.emit('detection:start', { motivation, options });
370
+ eventBusInstance.get('detection:start').next({ motivation, options });
385
371
  },
386
372
  getEventBus: () => eventBusInstance,
387
373
  };
@@ -25,7 +25,8 @@ import { useDetectionFlow } from '../../../hooks/useDetectionFlow';
25
25
  import { EventBusProvider, resetEventBusForTesting, useEventBus } from '../../../contexts/EventBusContext';
26
26
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
27
27
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
28
- import { SSEClient, resourceUri } from '@semiont/api-client';
28
+ import { SSEClient } from '@semiont/api-client';
29
+ import { resourceUri } from '@semiont/core';
29
30
 
30
31
  describe('Detection Progress Dismissal Bug', () => {
31
32
  let mockStream: any;
@@ -36,9 +37,6 @@ describe('Detection Progress Dismissal Bug', () => {
36
37
  vi.clearAllMocks();
37
38
 
38
39
  mockStream = {
39
- onProgress: vi.fn().mockReturnThis(),
40
- onComplete: vi.fn().mockReturnThis(),
41
- onError: vi.fn().mockReturnThis(),
42
40
  close: vi.fn(),
43
41
  };
44
42
 
@@ -85,7 +83,7 @@ describe('Detection Progress Dismissal Bug', () => {
85
83
 
86
84
  // User clicks detect button (emits detection:start)
87
85
  act(() => {
88
- eventBusInstance.emit('detection:start', {
86
+ eventBusInstance.get('detection:start').next({
89
87
  motivation: 'linking',
90
88
  options: { entityTypes: ['Location'] }
91
89
  });
@@ -98,7 +96,7 @@ describe('Detection Progress Dismissal Bug', () => {
98
96
 
99
97
  // SSE sends progress update
100
98
  act(() => {
101
- eventBusInstance.emit('detection:progress', {
99
+ eventBusInstance.get('detection:progress').next({
102
100
  status: 'scanning',
103
101
  message: 'Processing: Location',
104
102
  currentEntityType: 'Location',
@@ -112,7 +110,7 @@ describe('Detection Progress Dismissal Bug', () => {
112
110
 
113
111
  // Detection completes (SSE finishes, backend emits detection:complete)
114
112
  act(() => {
115
- eventBusInstance.emit('detection:complete', { motivation: 'linking' });
113
+ eventBusInstance.get('detection:complete').next({ motivation: 'linking' });
116
114
  });
117
115
 
118
116
  // detectingMotivation cleared immediately
@@ -151,15 +149,15 @@ describe('Detection Progress Dismissal Bug', () => {
151
149
 
152
150
  // First detection with stuck progress
153
151
  act(() => {
154
- eventBusInstance.emit('detection:start', { motivation: 'linking', options: {} });
152
+ eventBusInstance.get('detection:start').next({ motivation: 'linking', options: {} });
155
153
  });
156
154
 
157
155
  act(() => {
158
- eventBusInstance.emit('detection:progress', { message: 'Old progress stuck here' });
156
+ eventBusInstance.get('detection:progress').next({ message: 'Old progress stuck here' });
159
157
  });
160
158
 
161
159
  act(() => {
162
- eventBusInstance.emit('detection:complete', { motivation: 'linking' });
160
+ eventBusInstance.get('detection:complete').next({ motivation: 'linking' });
163
161
  });
164
162
 
165
163
  await waitFor(() => {
@@ -168,7 +166,7 @@ describe('Detection Progress Dismissal Bug', () => {
168
166
 
169
167
  // WORKAROUND: Start new detection clears old progress
170
168
  act(() => {
171
- eventBusInstance.emit('detection:start', { motivation: 'highlighting', options: {} });
169
+ eventBusInstance.get('detection:start').next({ motivation: 'highlighting', options: {} });
172
170
  });
173
171
 
174
172
  await waitFor(() => {
@@ -204,18 +202,18 @@ describe('Detection Progress Dismissal Bug', () => {
204
202
 
205
203
  // Show progress
206
204
  act(() => {
207
- eventBusInstance.emit('detection:start', { motivation: 'linking', options: {} });
205
+ eventBusInstance.get('detection:start').next({ motivation: 'linking', options: {} });
208
206
  });
209
207
 
210
208
  act(() => {
211
- eventBusInstance.emit('detection:progress', {
209
+ eventBusInstance.get('detection:progress').next({
212
210
  status: 'complete',
213
211
  message: 'Complete! Created 5 annotations'
214
212
  });
215
213
  });
216
214
 
217
215
  act(() => {
218
- eventBusInstance.emit('detection:complete', { motivation: 'linking' });
216
+ eventBusInstance.get('detection:complete').next({ motivation: 'linking' });
219
217
  });
220
218
 
221
219
  // Progress visible initially
@@ -231,12 +229,10 @@ describe('Detection Progress Dismissal Bug', () => {
231
229
  });
232
230
  });
233
231
 
234
- it('FIXED: useResolutionFlow now forwards final completion chunk data', async () => {
232
+ it('FIXED: SSE emits final completion chunk data as detection:progress', async () => {
235
233
  /**
236
- * This test verifies the fix for the useResolutionFlow bug.
237
- *
238
- * FIX: useResolutionFlow.ts stream.onComplete(finalChunk) now emits detection:progress
239
- * with the final chunk data BEFORE emitting detection:complete.
234
+ * This test verifies that SSE emits the final chunk as detection:progress
235
+ * BEFORE emitting detection:complete.
240
236
  *
241
237
  * This ensures the UI can display the final completion message with status:'complete'.
242
238
  */
@@ -255,20 +251,6 @@ describe('Detection Progress Dismissal Bug', () => {
255
251
  );
256
252
  }
257
253
 
258
- // Mock SSE stream to simulate backend behavior
259
- let onProgressCallback: any;
260
- let onCompleteCallback: any;
261
-
262
- mockStream.onProgress.mockImplementation((cb: any) => {
263
- onProgressCallback = cb;
264
- return mockStream;
265
- });
266
-
267
- mockStream.onComplete.mockImplementation((cb: any) => {
268
- onCompleteCallback = cb;
269
- return mockStream;
270
- });
271
-
272
254
  render(
273
255
  <EventBusProvider>
274
256
  <AuthTokenProvider token={null}>
@@ -281,15 +263,15 @@ describe('Detection Progress Dismissal Bug', () => {
281
263
 
282
264
  // Start detection (triggers SSE stream creation)
283
265
  act(() => {
284
- eventBusInstance.emit('detection:start', {
266
+ eventBusInstance.get('detection:start').next({
285
267
  motivation: 'linking',
286
268
  options: { entityTypes: ['Location'] }
287
269
  });
288
270
  });
289
271
 
290
- // Simulate SSE scanning chunk via stream.onProgress()
272
+ // Simulate SSE scanning chunk
291
273
  act(() => {
292
- onProgressCallback?.({
274
+ eventBusInstance.get('detection:progress').next({
293
275
  status: 'scanning',
294
276
  message: 'Processing: Location',
295
277
  });
@@ -299,10 +281,9 @@ describe('Detection Progress Dismissal Bug', () => {
299
281
  expect(screen.getByTestId('progress-status')).toHaveTextContent('scanning');
300
282
  });
301
283
 
302
- // Simulate backend sending final chunk to stream.onComplete(finalChunk)
303
- // useResolutionFlow should forward this as detection:progress
284
+ // Simulate SSE emitting final chunk as detection:progress
304
285
  act(() => {
305
- onCompleteCallback?.({
286
+ eventBusInstance.get('detection:progress').next({
306
287
  status: 'complete',
307
288
  message: 'Complete! Found 5 entities',
308
289
  foundCount: 5,