@semiont/react-ui 0.4.14 → 0.4.15

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 (49) hide show
  1. package/README.md +18 -12
  2. package/dist/KnowledgeBaseSessionContext-CpYaCbnC.d.mts +174 -0
  3. package/dist/{PdfAnnotationCanvas.client-CW6SKH2U.mjs → PdfAnnotationCanvas.client-CHDCGQBR.mjs} +3 -3
  4. package/dist/{chunk-HNZOXH4L.mjs → chunk-OZICDVH7.mjs} +5 -3
  5. package/dist/chunk-OZICDVH7.mjs.map +1 -0
  6. package/dist/chunk-R2U7P4TK.mjs +865 -0
  7. package/dist/chunk-R2U7P4TK.mjs.map +1 -0
  8. package/dist/{chunk-BQJWOK4C.mjs → chunk-VN5NY4SN.mjs} +9 -8
  9. package/dist/chunk-VN5NY4SN.mjs.map +1 -0
  10. package/dist/index.d.mts +139 -169
  11. package/dist/index.mjs +2197 -1947
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/test-utils.d.mts +13 -62
  14. package/dist/test-utils.mjs +40 -21
  15. package/dist/test-utils.mjs.map +1 -1
  16. package/package.json +5 -3
  17. package/src/components/ProtectedErrorBoundary.tsx +95 -0
  18. package/src/components/__tests__/ProtectedErrorBoundary.test.tsx +197 -0
  19. package/src/components/modals/PermissionDeniedModal.tsx +140 -0
  20. package/src/components/modals/ReferenceWizardModal.tsx +3 -2
  21. package/src/components/modals/SessionExpiredModal.tsx +101 -0
  22. package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +150 -0
  23. package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +115 -0
  24. package/src/components/resource/AnnotationHistory.tsx +5 -6
  25. package/src/components/resource/HistoryEvent.tsx +7 -7
  26. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +33 -34
  27. package/src/components/resource/__tests__/HistoryEvent.test.tsx +17 -19
  28. package/src/components/resource/__tests__/event-formatting.test.ts +70 -94
  29. package/src/components/resource/event-formatting.ts +56 -56
  30. package/src/components/resource/panels/ReferenceEntry.tsx +7 -5
  31. package/src/components/resource/panels/ResourceInfoPanel.tsx +8 -6
  32. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +12 -12
  33. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +1 -0
  34. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +1 -1
  35. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +4 -4
  36. package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +5 -10
  37. package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +23 -54
  38. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +6 -6
  39. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +7 -19
  40. package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +1 -1
  41. package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +18 -44
  42. package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +6 -6
  43. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +24 -26
  44. package/dist/TranslationManager-CudgH3gw.d.mts +0 -107
  45. package/dist/chunk-BQJWOK4C.mjs.map +0 -1
  46. package/dist/chunk-HNZOXH4L.mjs.map +0 -1
  47. package/dist/chunk-OL5UST25.mjs +0 -413
  48. package/dist/chunk-OL5UST25.mjs.map +0 -1
  49. /package/dist/{PdfAnnotationCanvas.client-CW6SKH2U.mjs.map → PdfAnnotationCanvas.client-CHDCGQBR.mjs.map} +0 -0
@@ -122,6 +122,7 @@ const renderWithEventBus = (component: React.ReactElement, tracker?: ReturnType<
122
122
 
123
123
  describe('ResourceInfoPanel Component', () => {
124
124
  const defaultProps = {
125
+ resourceId: 'test-resource-id',
125
126
  documentEntityTypes: [],
126
127
  documentLocale: undefined,
127
128
  primaryMediaType: undefined,
@@ -248,7 +248,7 @@ describe('Annotation creation clears pendingAnnotation', () => {
248
248
  const createdListener = vi.fn();
249
249
  // Set listener after first render so eventBus is captured
250
250
  await waitFor(() => expect(getEventBus()).toBeDefined());
251
- const subscription = getEventBus().get('mark:created').subscribe(createdListener);
251
+ const subscription = getEventBus().get('mark:create-ok').subscribe(createdListener);
252
252
 
253
253
  act(() => {
254
254
  emit('mark:requested', { selector: TEXT_SELECTOR, motivation: 'linking' });
@@ -27,6 +27,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
27
27
  import { render, waitFor } from '@testing-library/react';
28
28
  import { act } from 'react';
29
29
  import { useMarkFlow } from '../../../hooks/useMarkFlow';
30
+ import { useStoreTokenSync } from '../../../hooks/useStoreTokenSync';
30
31
  import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
31
32
 
32
33
  // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
@@ -69,8 +70,7 @@ describe('Annotation Deletion - Feature Integration', () => {
69
70
 
70
71
  function TestComponent() {
71
72
  eventBusInstance = useEventBus();
72
- // useMarkFlow is the single registration point for useBindFlow
73
- // (handles mark:delete mark:create annotate:detect-request, etc.)
73
+ useStoreTokenSync(); // Syncs auth token to namespace getToken
74
74
  useMarkFlow(testId);
75
75
  return null;
76
76
  }
@@ -138,7 +138,7 @@ describe('Annotation Deletion - Feature Integration', () => {
138
138
  const deletedListener = vi.fn();
139
139
 
140
140
  // Subscribe to success event
141
- eventBus.get('mark:deleted').subscribe(deletedListener);
141
+ eventBus.get('mark:delete-ok').subscribe(deletedListener);
142
142
 
143
143
  emitDelete('annotation-789');
144
144
 
@@ -175,7 +175,7 @@ describe('Annotation Deletion - Feature Integration', () => {
175
175
  // Verify failure event was emitted
176
176
  await waitFor(() => {
177
177
  expect(failedListener).toHaveBeenCalledWith({
178
- error: expect.any(Error),
178
+ message: expect.any(String),
179
179
  });
180
180
  });
181
181
  });
@@ -25,7 +25,7 @@ import { useMarkFlow } from '../../../hooks/useMarkFlow';
25
25
  import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
26
26
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
27
27
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
28
- import { SSEClient } from '@semiont/api-client';
28
+ import { SemiontApiClient } from '@semiont/api-client';
29
29
  import { resourceId } from '@semiont/core';
30
30
 
31
31
  // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
@@ -39,20 +39,15 @@ vi.mock('../../../components/Toast', () => ({
39
39
  }));
40
40
 
41
41
  describe('Detection Progress Dismissal Bug', () => {
42
- let mockStream: any;
43
42
  const rUri = resourceId('test');
44
43
 
45
44
  beforeEach(() => {
46
45
  vi.clearAllMocks();
47
46
 
48
- mockStream = {
49
- close: vi.fn(),
50
- };
51
-
52
- vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue(mockStream);
53
- vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue(mockStream);
54
- vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue(mockStream);
55
- vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue(mockStream);
47
+ vi.spyOn(SemiontApiClient.prototype, 'annotateReferences').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
48
+ vi.spyOn(SemiontApiClient.prototype, 'annotateHighlights').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
49
+ vi.spyOn(SemiontApiClient.prototype, 'annotateComments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
50
+ vi.spyOn(SemiontApiClient.prototype, 'annotateAssessments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
56
51
  });
57
52
 
58
53
  afterEach(() => {
@@ -2,14 +2,14 @@
2
2
  * Layer 3: Feature Integration Test - Bind Flow (body update)
3
3
  *
4
4
  * Tests the write side of useBindFlow:
5
- * - bind:update-body → calls bindAnnotation API
6
- * - bind:update-body → emits bind:body-updated on success
5
+ * - bind:update-body → calls http.bindAnnotation API (plain POST)
7
6
  * - bind:update-body → emits bind:body-update-failed on error
8
7
  * - auth token passed to bindAnnotation
9
8
  *
10
- * The wizard modal (ReferenceWizardModal) handles modal state, context
11
- * gathering, search configuration, and result display. This test covers
12
- * only the downstream API calls after the wizard emits bind:update-body.
9
+ * After the UNIFIED-STREAM migration, bind is a plain POST returning
10
+ * {correlationId}. The state change arrives on the events-stream as
11
+ * mark:body-updated. These tests focus on the POST call, not the
12
+ * events-stream delivery (which is tested in AnnotationStore tests).
13
13
  *
14
14
  * Uses real providers (EventBus, ApiClient, AuthToken) with mocked API boundary.
15
15
  */
@@ -21,14 +21,15 @@ import { useBindFlow } from '../../../hooks/useBindFlow';
21
21
  import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
22
22
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
23
23
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
24
- import { SSEClient } from '@semiont/api-client';
25
- import { resourceId, accessToken, annotationId } from '@semiont/core';
24
+ import { SemiontApiClient } from '@semiont/api-client';
25
+ import { resourceId, annotationId } from '@semiont/core';
26
+
27
+ const mockShowError = vi.fn();
26
28
 
27
- // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
28
29
  vi.mock('../../../components/Toast', () => ({
29
30
  useToast: () => ({
30
31
  showSuccess: vi.fn(),
31
- showError: vi.fn(),
32
+ showError: mockShowError,
32
33
  showInfo: vi.fn(),
33
34
  showWarning: vi.fn(),
34
35
  }),
@@ -43,11 +44,9 @@ describe('Bind Flow - Body Update Integration', () => {
43
44
  beforeEach(() => {
44
45
  vi.clearAllMocks();
45
46
 
46
- bindAnnotationSpy = vi.fn().mockImplementation((_rId: any, annId: any, _req: any, opts: any) => {
47
- queueMicrotask(() => opts.eventBus.get('bind:finished').next({ annotationId: annId }));
48
- return { close: vi.fn() };
49
- });
50
- vi.spyOn(SSEClient.prototype, 'bindAnnotation').mockImplementation(bindAnnotationSpy as any);
47
+ // Mock the HTTP bindAnnotation method (plain POST, returns {correlationId})
48
+ bindAnnotationSpy = vi.fn().mockResolvedValue({ correlationId: 'corr-test' });
49
+ vi.spyOn(SemiontApiClient.prototype, 'bindAnnotation').mockImplementation(bindAnnotationSpy as any);
51
50
  });
52
51
 
53
52
  afterEach(() => {
@@ -82,10 +81,11 @@ describe('Bind Flow - Body Update Integration', () => {
82
81
 
83
82
  // ─── bind:update-body ──────────────────────────────────────────────────
84
83
 
85
- it('bind:update-body calls bindAnnotation API', async () => {
84
+ it('bind:update-body calls http.bindAnnotation (plain POST)', async () => {
86
85
  const { getEventBus } = renderBindFlow();
87
86
 
88
87
  act(() => { getEventBus().get('bind:update-body').next({
88
+ correlationId: 'corr-1',
89
89
  annotationId: annotationId('ann-body-1'),
90
90
  resourceId: resourceId('linked-resource-id'),
91
91
  operations: [{ op: 'add', item: { type: 'SpecificResource' as const, source: 'linked-resource-id' } }],
@@ -100,6 +100,7 @@ describe('Bind Flow - Body Update Integration', () => {
100
100
  const { getEventBus } = renderBindFlow();
101
101
 
102
102
  act(() => { getEventBus().get('bind:update-body').next({
103
+ correlationId: 'corr-2',
103
104
  annotationId: annotationId('ann-auth'),
104
105
  resourceId: resourceId('resource-id'),
105
106
  operations: [{ op: 'replace', newItem: { type: 'SpecificResource' as const, source: 'resource-id' } }],
@@ -111,57 +112,24 @@ describe('Bind Flow - Body Update Integration', () => {
111
112
 
112
113
  const callArgs = bindAnnotationSpy.mock.calls[0];
113
114
  expect(callArgs[3]).toHaveProperty('auth');
114
- expect(callArgs[3].auth).toBe(accessToken(testToken));
115
- });
116
-
117
- it('bind:update-body emits bind:body-updated on success', async () => {
118
- const { getEventBus } = renderBindFlow();
119
- const bodyUpdatedSpy = vi.fn();
120
-
121
- const subscription = getEventBus().get('bind:body-updated').subscribe(bodyUpdatedSpy);
122
-
123
- act(() => { getEventBus().get('bind:update-body').next({
124
- annotationId: annotationId('ann-success'),
125
- resourceId: resourceId('resource-id'),
126
- operations: [{ op: 'add', item: { type: 'SpecificResource' as const, source: 'resource-id' } }],
127
- }); });
128
-
129
- await waitFor(() => {
130
- expect(bodyUpdatedSpy).toHaveBeenCalledTimes(1);
131
- });
132
-
133
- subscription.unsubscribe();
134
-
135
- expect(bodyUpdatedSpy).toHaveBeenCalledWith({
136
- annotationId: annotationId('ann-success'),
137
- });
138
115
  });
139
116
 
140
- it('bind:update-body emits bind:body-update-failed on API error', async () => {
141
- bindAnnotationSpy.mockImplementation((_rId: any, _annId: any, _req: any, opts: any) => {
142
- queueMicrotask(() => opts.eventBus.get('bind:failed').next({ error: new Error('Update failed') }));
143
- return { close: vi.fn() };
144
- });
117
+ it('bind:update-body shows error toast on API error', async () => {
118
+ bindAnnotationSpy.mockRejectedValueOnce(new Error('Update failed'));
145
119
 
146
120
  const { getEventBus } = renderBindFlow();
147
- const bodyUpdateFailedSpy = vi.fn();
148
-
149
- const subscription = getEventBus().get('bind:body-update-failed').subscribe(bodyUpdateFailedSpy);
150
121
 
151
122
  act(() => { getEventBus().get('bind:update-body').next({
123
+ correlationId: 'corr-3',
152
124
  annotationId: annotationId('ann-fail'),
153
125
  resourceId: resourceId('resource-id'),
154
126
  operations: [{ op: 'remove', item: { type: 'SpecificResource' as const, source: 'old-id' } }],
155
127
  }); });
156
128
 
157
129
  await waitFor(() => {
158
- expect(bodyUpdateFailedSpy).toHaveBeenCalledTimes(1);
159
- });
160
-
161
- subscription.unsubscribe();
162
-
163
- expect(bodyUpdateFailedSpy).toHaveBeenCalledWith({
164
- error: expect.any(Error),
130
+ expect(mockShowError).toHaveBeenCalledWith(
131
+ expect.stringContaining('Update failed'),
132
+ );
165
133
  });
166
134
  });
167
135
 
@@ -169,6 +137,7 @@ describe('Bind Flow - Body Update Integration', () => {
169
137
  const { getEventBus } = renderBindFlow();
170
138
 
171
139
  act(() => { getEventBus().get('bind:update-body').next({
140
+ correlationId: 'corr-4',
172
141
  annotationId: annotationId('ann-dedup'),
173
142
  resourceId: resourceId('resource-id'),
174
143
  operations: [{ op: 'add', item: { type: 'SpecificResource' as const, source: 'resource-id' } }],
@@ -17,7 +17,7 @@ import { useMarkFlow } from '../../../hooks/useMarkFlow';
17
17
  import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
18
18
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
19
19
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
20
- import { SSEClient } from '@semiont/api-client';
20
+ import { SemiontApiClient } from '@semiont/api-client';
21
21
 
22
22
  // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
23
23
  vi.mock('../../../components/Toast', () => ({
@@ -33,11 +33,11 @@ describe('REPRODUCING BUG: Detection state not updating', () => {
33
33
  beforeEach(() => {
34
34
  vi.clearAllMocks();
35
35
 
36
- // Minimal mock - SSE streams not needed for this test
37
- vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
38
- vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
39
- vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
40
- vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
36
+ // Minimal mock namespace methods call these HTTP methods internally
37
+ vi.spyOn(SemiontApiClient.prototype, 'annotateReferences').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
38
+ vi.spyOn(SemiontApiClient.prototype, 'annotateHighlights').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
39
+ vi.spyOn(SemiontApiClient.prototype, 'annotateComments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
40
+ vi.spyOn(SemiontApiClient.prototype, 'annotateAssessments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
41
41
  });
42
42
 
43
43
  afterEach(() => {
@@ -29,7 +29,7 @@ import { useMarkFlow } from '../../../hooks/useMarkFlow';
29
29
  import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
30
30
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
31
31
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
32
- import { SSEClient } from '@semiont/api-client';
32
+ import { SemiontApiClient } from '@semiont/api-client';
33
33
  import type { Motivation } from '@semiont/core';
34
34
  import { resourceId } from '@semiont/core';
35
35
  import type { Emitter } from 'mitt';
@@ -45,15 +45,7 @@ vi.mock('../../../components/Toast', () => ({
45
45
  }));
46
46
  import type { EventMap } from '@semiont/core';
47
47
 
48
- // Mock SSE stream - SSE now emits directly to EventBus, no callbacks
49
- const createMockSSEStream = () => {
50
- return {
51
- close: vi.fn(),
52
- };
53
- };
54
-
55
48
  describe('Detection Flow - Feature Integration', () => {
56
- let mockStream: ReturnType<typeof createMockSSEStream>;
57
49
  let markReferencesSpy: any;
58
50
  let markHighlightsSpy: any;
59
51
  let detectCommentsSpy: any;
@@ -61,14 +53,11 @@ describe('Detection Flow - Feature Integration', () => {
61
53
  beforeEach(() => {
62
54
  vi.clearAllMocks();
63
55
 
64
- // Create fresh mock stream for each test
65
- mockStream = createMockSSEStream();
66
-
67
- // Spy on SSEClient prototype methods
68
- markReferencesSpy = vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue(mockStream as any);
69
- markHighlightsSpy = vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue(mockStream as any);
70
- detectCommentsSpy = vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue(mockStream as any);
71
- vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue(mockStream as any);
56
+ // Spy on SemiontApiClient prototype HTTP methods (namespace methods call these)
57
+ markReferencesSpy = vi.spyOn(SemiontApiClient.prototype, 'annotateReferences').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
58
+ markHighlightsSpy = vi.spyOn(SemiontApiClient.prototype, 'annotateHighlights').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
59
+ detectCommentsSpy = vi.spyOn(SemiontApiClient.prototype, 'annotateComments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
60
+ vi.spyOn(SemiontApiClient.prototype, 'annotateAssessments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
72
61
  });
73
62
 
74
63
  afterEach(() => {
@@ -298,8 +287,7 @@ describe('Detection Flow - Feature Integration', () => {
298
287
 
299
288
  // Reset for next test
300
289
  vi.clearAllMocks();
301
- mockStream = createMockSSEStream();
302
- detectCommentsSpy.mockReturnValue(mockStream);
290
+ detectCommentsSpy.mockResolvedValue({ correlationId: 'c2', jobId: 'j2' });
303
291
 
304
292
  // Test commenting
305
293
  act(() => {
@@ -158,7 +158,7 @@ describe('Toast Notifications - Verifies Toast Integration', () => {
158
158
  // Emit generation failed event
159
159
  act(() => {
160
160
  eventBusInstance.get('yield:failed').next({
161
- error: new Error('Failed to generate document'),
161
+ error: 'Failed to generate document',
162
162
  });
163
163
  });
164
164
 
@@ -26,10 +26,9 @@ import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext
26
26
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
27
27
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
28
28
  import { useBindFlow } from '../../../hooks/useBindFlow';
29
- import { SSEClient } from '@semiont/api-client';
29
+ import { SemiontApiClient } from '@semiont/api-client';
30
30
  import type { AnnotationId, ResourceId } from '@semiont/core';
31
31
  import { resourceId, annotationId } from '@semiont/core';
32
- import type { Emitter } from 'mitt';
33
32
  import type { EventMap } from '@semiont/core';
34
33
 
35
34
  // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
@@ -42,15 +41,7 @@ vi.mock('../../../components/Toast', () => ({
42
41
  }),
43
42
  }));
44
43
 
45
- // Mock SSE stream - SSE now emits directly to EventBus, no callbacks
46
- const createMockGenerationStream = () => {
47
- return {
48
- close: vi.fn(),
49
- };
50
- };
51
-
52
44
  describe('Generation Flow - Feature Integration', () => {
53
- let mockStream: ReturnType<typeof createMockGenerationStream>;
54
45
  let generateResourceSpy: any;
55
46
  let mockShowSuccess: ReturnType<typeof vi.fn>;
56
47
  let mockShowError: ReturnType<typeof vi.fn>;
@@ -59,11 +50,8 @@ describe('Generation Flow - Feature Integration', () => {
59
50
  beforeEach(() => {
60
51
  vi.clearAllMocks();
61
52
 
62
- // Create fresh mock stream for each test
63
- mockStream = createMockGenerationStream();
64
-
65
- // Spy on SSEClient prototype method
66
- generateResourceSpy = vi.spyOn(SSEClient.prototype, 'yieldResource').mockReturnValue(mockStream as any);
53
+ // Spy on SemiontApiClient prototype HTTP method (namespace methods call this)
54
+ generateResourceSpy = vi.spyOn(SemiontApiClient.prototype, 'yieldResourceFromAnnotation').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
67
55
 
68
56
  // Mock callbacks
69
57
  mockShowSuccess = vi.fn();
@@ -107,17 +95,13 @@ describe('Generation Flow - Feature Integration', () => {
107
95
  expect(generateResourceSpy).toHaveBeenCalledWith(
108
96
  testResourceId,
109
97
  testAnnotationId,
110
- {
98
+ expect.objectContaining({
111
99
  title: 'Generated Document',
112
100
  prompt: 'Create a comprehensive document',
113
101
  language: 'en',
114
102
  temperature: 0.7,
115
103
  maxTokens: 2000,
116
- context: {
117
- sourceText: 'Reference text from the document',
118
- entityTypes: ['Person', 'Organization'],
119
- },
120
- },
104
+ }),
121
105
  expect.objectContaining({ auth: undefined })
122
106
  );
123
107
  });
@@ -295,7 +279,7 @@ describe('Generation Flow - Feature Integration', () => {
295
279
 
296
280
  // Emit failure
297
281
  act(() => {
298
- getEventBus().get('yield:failed').next({ error: new Error('Network error') });
282
+ getEventBus().get('yield:failed').next({ error: 'Network error' });
299
283
  });
300
284
 
301
285
  // Verify: progress cleared and not generating
@@ -305,18 +289,14 @@ describe('Generation Flow - Feature Integration', () => {
305
289
  });
306
290
  });
307
291
 
308
- it('should only call API once even with multiple event listeners', async () => {
292
+ it('should only call API once even with multiple renders', async () => {
309
293
  const testResourceId = resourceId('test-resource');
310
294
  const testAnnotationId = annotationId('test-annotation');
311
295
 
312
- const { emitGenerationStart, getEventBus } = renderYieldFlow(
296
+ const { emitGenerationStart } = renderYieldFlow(
313
297
  testResourceId
314
298
  );
315
299
 
316
- // Add an additional event listener (simulating multiple subscribers)
317
- const additionalListener = vi.fn();
318
- const subscription = getEventBus().get('yield:request').subscribe(additionalListener);
319
-
320
300
  // Trigger generation
321
301
  act(() => {
322
302
  emitGenerationStart(testAnnotationId, testResourceId, {
@@ -330,13 +310,8 @@ describe('Generation Flow - Feature Integration', () => {
330
310
  expect(generateResourceSpy).toHaveBeenCalled();
331
311
  });
332
312
 
333
- // VERIFY: API called exactly once, even though multiple listeners exist
313
+ // VERIFY: API called exactly once
334
314
  expect(generateResourceSpy).toHaveBeenCalledTimes(1);
335
-
336
- // VERIFY: Our additional listener was called (events work)
337
- expect(additionalListener).toHaveBeenCalledTimes(1);
338
-
339
- subscription.unsubscribe();
340
315
  });
341
316
 
342
317
  it('should forward final chunk as progress before emitting complete', async () => {
@@ -385,15 +360,13 @@ describe('Generation Flow - Feature Integration', () => {
385
360
  function renderYieldFlow(
386
361
  testResourceId: ResourceId
387
362
  ) {
388
- let eventBusInstance: Emitter<EventMap>;
363
+ let eventBusInstance: ReturnType<typeof useEventBus>;
364
+ let generateFn: ReturnType<typeof useYieldFlow>['onGenerateDocument'];
389
365
 
390
366
  // Component to capture EventBus instance and set up event operations
391
367
  function EventBusCapture() {
392
368
  eventBusInstance = useEventBus();
393
-
394
- // Set up resolution flow (resolve:update-body, resolve:link)
395
369
  useBindFlow(testResourceId);
396
-
397
370
  return null;
398
371
  }
399
372
 
@@ -402,12 +375,15 @@ function renderYieldFlow(
402
375
  const {
403
376
  isGenerating,
404
377
  generationProgress,
378
+ onGenerateDocument,
405
379
  } = useYieldFlow(
406
380
  'en',
407
381
  testResourceId,
408
382
  vi.fn()
409
383
  );
410
384
 
385
+ generateFn = onGenerateDocument;
386
+
411
387
  return (
412
388
  <div>
413
389
  <div data-testid="is-generating">
@@ -434,9 +410,10 @@ function renderYieldFlow(
434
410
  return {
435
411
  emitGenerationStart: (
436
412
  aId: AnnotationId,
437
- rId: ResourceId,
413
+ _rId: ResourceId,
438
414
  options: {
439
415
  title: string;
416
+ storageUri?: string;
440
417
  prompt?: string;
441
418
  language?: string;
442
419
  temperature?: number;
@@ -444,11 +421,8 @@ function renderYieldFlow(
444
421
  context: any;
445
422
  }
446
423
  ) => {
447
- eventBusInstance.get('yield:request').next({
448
- annotationId: aId,
449
- resourceId: rId,
450
- options,
451
- });
424
+ // Call the hook's callback directly (no longer EventBus-driven)
425
+ generateFn(aId as string, { storageUri: options.storageUri ?? 'file:///tmp/test', ...options });
452
426
  },
453
427
  getEventBus: () => eventBusInstance,
454
428
  };
@@ -31,7 +31,7 @@ import { useMarkFlow } from '../../../hooks/useMarkFlow';
31
31
  import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
32
32
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
33
33
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
34
- import { SSEClient } from '@semiont/api-client';
34
+ import { SemiontApiClient } from '@semiont/api-client';
35
35
  import type { components } from '@semiont/core';
36
36
 
37
37
  // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
@@ -167,11 +167,11 @@ describe('Detection Progress Flow Integration (Layer 3)', () => {
167
167
  // Create fresh stream for each test
168
168
  mockStream = new MockSSEStream();
169
169
 
170
- // Spy on SSEClient prototype methods to inject mock stream
171
- vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue(mockStream as any);
172
- vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue(mockStream as any);
173
- vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue(mockStream as any);
174
- vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue(mockStream as any);
170
+ // Spy on SemiontApiClient prototype HTTP methods (namespace methods call these)
171
+ vi.spyOn(SemiontApiClient.prototype, 'annotateHighlights').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
172
+ vi.spyOn(SemiontApiClient.prototype, 'annotateAssessments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
173
+ vi.spyOn(SemiontApiClient.prototype, 'annotateComments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
174
+ vi.spyOn(SemiontApiClient.prototype, 'annotateReferences').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
175
175
 
176
176
  mockAnnotations = [];
177
177
  });
@@ -7,7 +7,7 @@
7
7
 
8
8
  import React, { useState, useEffect, useCallback, useMemo } from 'react';
9
9
  import { useQueryClient } from '@tanstack/react-query';
10
- import type { components, ResourceId, ResourceEvent, GatheredContext } from '@semiont/core';
10
+ import type { components, ResourceId, GatheredContext, EventMap } from '@semiont/core';
11
11
  import { annotationId } from '@semiont/core';
12
12
  import { getLanguage, getPrimaryRepresentation, getPrimaryMediaType, getMimeCategory } from '@semiont/api-client';
13
13
  import { ANNOTATORS } from '@semiont/react-ui';
@@ -140,7 +140,7 @@ export function ResourceViewerPage({
140
140
 
141
141
  // Get unified event bus for subscribing to UI events
142
142
  const eventBus = useEventBus();
143
- const client = useApiClient();
143
+ const semiont = useApiClient();
144
144
  const queryClient = useQueryClient(); // retained for non-store queries (events log)
145
145
 
146
146
  // UI state hooks
@@ -164,17 +164,17 @@ export function ResourceViewerPage({
164
164
 
165
165
  // Binary path: fetch short-lived media token, construct URL
166
166
  const { token: mediaToken, loading: mediaTokenLoading } = useMediaToken(rUri);
167
- const binaryContent = (isBinary && mediaToken && client)
168
- ? `${client.baseUrl}/api/resources/${rUri}?token=${mediaToken}`
167
+ const binaryContent = (isBinary && mediaToken && semiont)
168
+ ? `${semiont.baseUrl}/api/resources/${rUri}?token=${mediaToken}`
169
169
  : '';
170
170
 
171
171
  const content = isBinary ? binaryContent : textContent;
172
172
  const contentLoading = isBinary ? mediaTokenLoading : textLoading;
173
173
 
174
- const annotationsData = useObservable(client.stores.annotations.listForResource(rUri));
174
+ const annotationsData = useObservable(semiont.browse.annotations(rUri));
175
175
  const annotations = useMemo(
176
- () => annotationsData?.annotations || [],
177
- [annotationsData?.annotations]
176
+ () => annotationsData || [],
177
+ [annotationsData]
178
178
  );
179
179
 
180
180
  const { data: referencedByData, isLoading: referencedByLoading } = resources.referencedBy.useQuery(rUri);
@@ -209,8 +209,8 @@ export function ResourceViewerPage({
209
209
  setWizardEntityTypes(event.entityTypes);
210
210
  setWizardOpen(true);
211
211
 
212
- // Trigger context gathering
213
- eventBus.get('gather:requested').next({ correlationId: crypto.randomUUID(), annotationId: event.annotationId, resourceId: event.resourceId });
212
+ // Trigger context gathering — gather:requested is consumed by useContextGatherFlow
213
+ eventBus.get('gather:requested').next({ correlationId: crypto.randomUUID(), annotationId: event.annotationId, resourceId: event.resourceId, options: { contextWindow: 2000 } });
214
214
  });
215
215
  return () => subscription.unsubscribe();
216
216
  }, [eventBus]);
@@ -231,21 +231,18 @@ export function ResourceViewerPage({
231
231
  });
232
232
  }, [onGenerateDocument]);
233
233
 
234
- const handleWizardLinkResource = useCallback((referenceId: string, targetResourceId: string) => {
235
- eventBus.get('bind:update-body').next({
236
- annotationId: annotationId(referenceId),
237
- resourceId: rUri,
238
- operations: [{
239
- op: 'add',
240
- item: {
241
- type: 'SpecificResource' as const,
242
- source: targetResourceId,
243
- purpose: 'linking' as const,
244
- },
245
- }],
246
- });
247
- showSuccess('Reference linked successfully');
248
- }, [rUri, showSuccess]); // eventBus is stable singleton
234
+ const handleWizardLinkResource = useCallback(async (referenceId: string, targetResourceId: string) => {
235
+ try {
236
+ await semiont.bind.body(
237
+ rUri,
238
+ annotationId(referenceId),
239
+ [{ op: 'add', item: { type: 'SpecificResource' as const, source: targetResourceId, purpose: 'linking' as const } }],
240
+ );
241
+ showSuccess('Reference linked successfully');
242
+ } catch (error) {
243
+ showError(`Failed to link reference: ${error instanceof Error ? error.message : String(error)}`);
244
+ }
245
+ }, [rUri, semiont, showSuccess, showError]);
249
246
 
250
247
  const handleWizardComposeNavigate = useCallback((
251
248
  context: GatheredContext,
@@ -364,8 +361,8 @@ export function ResourceViewerPage({
364
361
  triggerSparkleAnimation(annotationId);
365
362
  }, [triggerSparkleAnimation]);
366
363
 
367
- const handleAnnotationAdded = useCallback((event: Extract<ResourceEvent, { type: 'annotation.added' }>) => {
368
- triggerSparkleAnimation(event.payload.annotation.id);
364
+ const handleAnnotationAdded = useCallback((stored: EventMap['mark:added']) => {
365
+ triggerSparkleAnimation(stored.payload.annotation.id);
369
366
  }, [triggerSparkleAnimation]);
370
367
 
371
368
  const handleAnnotationCreateFailed = useCallback(() => showError('Failed to create annotation'), [showError]);
@@ -615,6 +612,7 @@ export function ResourceViewerPage({
615
612
  {/* Document Info Panel */}
616
613
  {activePanel === 'info' && (
617
614
  <ResourceInfoPanel
615
+ resourceId={rUri}
618
616
  documentEntityTypes={documentEntityTypes}
619
617
  documentLocale={getLanguage(resource)}
620
618
  primaryMediaType={primaryMediaType}