@semiont/react-ui 0.4.2 → 0.4.4

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 (163) hide show
  1. package/dist/{PdfAnnotationCanvas.client-PVTVPDBQ.mjs → PdfAnnotationCanvas.client-LF6DDTCV.mjs} +3 -3
  2. package/dist/{ar-5REA6P4J.mjs → ar-RJLR7NDD.mjs} +13 -11
  3. package/dist/ar-RJLR7NDD.mjs.map +1 -0
  4. package/dist/{bn-YHRYHHPD.mjs → bn-4NU2UAZ7.mjs} +13 -11
  5. package/dist/bn-4NU2UAZ7.mjs.map +1 -0
  6. package/dist/{chunk-VVCCMJS7.mjs → chunk-2VWLGQIO.mjs} +13 -11
  7. package/dist/chunk-2VWLGQIO.mjs.map +1 -0
  8. package/dist/chunk-5JZFKRLW.mjs +62 -0
  9. package/dist/chunk-5JZFKRLW.mjs.map +1 -0
  10. package/dist/{chunk-PFQYNPQJ.mjs → chunk-EKQMINCV.mjs} +61 -92
  11. package/dist/chunk-EKQMINCV.mjs.map +1 -0
  12. package/dist/{chunk-ZPV43WN2.mjs → chunk-XMCUHQ2Y.mjs} +72 -3
  13. package/dist/chunk-XMCUHQ2Y.mjs.map +1 -0
  14. package/dist/{cs-JTJXTX2T.mjs → cs-6UJZFF55.mjs} +13 -11
  15. package/dist/cs-6UJZFF55.mjs.map +1 -0
  16. package/dist/{da-MK37SJB6.mjs → da-CFFBBOH3.mjs} +13 -11
  17. package/dist/da-CFFBBOH3.mjs.map +1 -0
  18. package/dist/{de-LGBCWERA.mjs → de-2KAG6KMX.mjs} +13 -11
  19. package/dist/de-2KAG6KMX.mjs.map +1 -0
  20. package/dist/{el-FKJMFCWY.mjs → el-KVPQ7VNF.mjs} +13 -11
  21. package/dist/el-KVPQ7VNF.mjs.map +1 -0
  22. package/dist/{en-AOSMPC2M.mjs → en-ZW4UKTVX.mjs} +2 -2
  23. package/dist/{es-LVDPIXWU.mjs → es-ZU2ECPVG.mjs} +13 -11
  24. package/dist/es-ZU2ECPVG.mjs.map +1 -0
  25. package/dist/{fa-3VA2PIUD.mjs → fa-CBIVKDIQ.mjs} +13 -11
  26. package/dist/fa-CBIVKDIQ.mjs.map +1 -0
  27. package/dist/{fi-3WM75ZLR.mjs → fi-5HUVT7IK.mjs} +13 -11
  28. package/dist/fi-5HUVT7IK.mjs.map +1 -0
  29. package/dist/{fr-NK4A72WA.mjs → fr-O5WU2US2.mjs} +13 -11
  30. package/dist/fr-O5WU2US2.mjs.map +1 -0
  31. package/dist/{he-IACZDZMB.mjs → he-WP4C2SNJ.mjs} +13 -11
  32. package/dist/he-WP4C2SNJ.mjs.map +1 -0
  33. package/dist/{hi-JZ7MGMMS.mjs → hi-HLQXNRWI.mjs} +13 -11
  34. package/dist/hi-HLQXNRWI.mjs.map +1 -0
  35. package/dist/{id-P3KDQGNK.mjs → id-DTJT2ZA6.mjs} +13 -11
  36. package/dist/id-DTJT2ZA6.mjs.map +1 -0
  37. package/dist/index.css +213 -0
  38. package/dist/index.css.map +1 -1
  39. package/dist/index.d.mts +48 -50
  40. package/dist/index.mjs +1492 -1474
  41. package/dist/index.mjs.map +1 -1
  42. package/dist/{it-LQS33SUY.mjs → it-2DK57IMT.mjs} +13 -11
  43. package/dist/it-2DK57IMT.mjs.map +1 -0
  44. package/dist/{ja-G4FKZPWD.mjs → ja-52ZNY72Y.mjs} +13 -11
  45. package/dist/ja-52ZNY72Y.mjs.map +1 -0
  46. package/dist/{ko-2XWKQ7BA.mjs → ko-4Z2IMYZO.mjs} +13 -11
  47. package/dist/ko-4Z2IMYZO.mjs.map +1 -0
  48. package/dist/{ms-2SNONIUD.mjs → ms-HKU73KEX.mjs} +13 -11
  49. package/dist/ms-HKU73KEX.mjs.map +1 -0
  50. package/dist/{nl-BMZUAJ7J.mjs → nl-HPHQMXPT.mjs} +13 -11
  51. package/dist/nl-HPHQMXPT.mjs.map +1 -0
  52. package/dist/{no-6J3WIZ6L.mjs → no-Q7G4PVFT.mjs} +13 -11
  53. package/dist/no-Q7G4PVFT.mjs.map +1 -0
  54. package/dist/{pl-QQ7DAUVK.mjs → pl-D43C2CE3.mjs} +13 -11
  55. package/dist/pl-D43C2CE3.mjs.map +1 -0
  56. package/dist/{pt-MU3GN7MW.mjs → pt-V3GFSY7B.mjs} +13 -11
  57. package/dist/pt-V3GFSY7B.mjs.map +1 -0
  58. package/dist/{ro-6GBE72QK.mjs → ro-3UIRV3OA.mjs} +13 -11
  59. package/dist/ro-3UIRV3OA.mjs.map +1 -0
  60. package/dist/{sv-NQIL7PEM.mjs → sv-BMW26A7M.mjs} +13 -11
  61. package/dist/sv-BMW26A7M.mjs.map +1 -0
  62. package/dist/test-utils.mjs +4 -4
  63. package/dist/{th-6OCNZQBE.mjs → th-CMND2QET.mjs} +13 -11
  64. package/dist/th-CMND2QET.mjs.map +1 -0
  65. package/dist/{tr-XWJ5P3SC.mjs → tr-P3AD7MYA.mjs} +13 -11
  66. package/dist/tr-P3AD7MYA.mjs.map +1 -0
  67. package/dist/{uk-AKSN6DGW.mjs → uk-QPONRQ5O.mjs} +13 -11
  68. package/dist/uk-QPONRQ5O.mjs.map +1 -0
  69. package/dist/{vi-23GHQ45M.mjs → vi-G2OMVMCI.mjs} +13 -11
  70. package/dist/vi-G2OMVMCI.mjs.map +1 -0
  71. package/dist/{zh-ITT4QBSN.mjs → zh-HTDN4LKE.mjs} +13 -11
  72. package/dist/zh-HTDN4LKE.mjs.map +1 -0
  73. package/package.json +3 -5
  74. package/src/components/CodeMirrorRenderer.tsx +9 -9
  75. package/src/components/ResourceTagsInline.tsx +1 -1
  76. package/src/components/__tests__/ResourceTagsInline.test.tsx +3 -3
  77. package/src/components/modals/ContextSummary.tsx +56 -165
  78. package/src/components/modals/GatherContextStep.tsx +124 -42
  79. package/src/components/modals/ReferenceWizardModal.tsx +8 -25
  80. package/src/components/modals/SearchModal.css +241 -0
  81. package/src/components/resource/AnnotateView.tsx +3 -3
  82. package/src/components/resource/BrowseView.tsx +2 -2
  83. package/src/components/resource/ResourceViewer.tsx +4 -4
  84. package/src/components/resource/__tests__/annotation-interactions.test.tsx +1 -1
  85. package/src/components/resource/panels/AssessmentPanel.tsx +3 -0
  86. package/src/components/resource/panels/AssistSection.tsx +4 -1
  87. package/src/components/resource/panels/CommentsPanel.tsx +3 -0
  88. package/src/components/resource/panels/ResourceInfoPanel.tsx +82 -0
  89. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +4 -0
  90. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +94 -0
  91. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +6 -6
  92. package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +4 -4
  93. package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +23 -17
  94. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +4 -4
  95. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +14 -14
  96. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +2 -2
  97. package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +2 -2
  98. package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +4 -4
  99. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +12 -10
  100. package/translations/ar.json +12 -10
  101. package/translations/bn.json +12 -10
  102. package/translations/cs.json +12 -10
  103. package/translations/da.json +12 -10
  104. package/translations/de.json +12 -10
  105. package/translations/el.json +12 -10
  106. package/translations/en.json +13 -11
  107. package/translations/es.json +12 -10
  108. package/translations/fa.json +12 -10
  109. package/translations/fi.json +12 -10
  110. package/translations/fr.json +12 -10
  111. package/translations/he.json +12 -10
  112. package/translations/hi.json +12 -10
  113. package/translations/id.json +12 -10
  114. package/translations/it.json +12 -10
  115. package/translations/ja.json +12 -10
  116. package/translations/ko.json +12 -10
  117. package/translations/ms.json +12 -10
  118. package/translations/nl.json +12 -10
  119. package/translations/no.json +12 -10
  120. package/translations/pl.json +12 -10
  121. package/translations/pt.json +12 -10
  122. package/translations/ro.json +12 -10
  123. package/translations/sv.json +12 -10
  124. package/translations/th.json +12 -10
  125. package/translations/tr.json +12 -10
  126. package/translations/uk.json +12 -10
  127. package/translations/vi.json +12 -10
  128. package/translations/zh.json +12 -10
  129. package/dist/ar-5REA6P4J.mjs.map +0 -1
  130. package/dist/bn-YHRYHHPD.mjs.map +0 -1
  131. package/dist/chunk-2HGWOLVN.mjs +0 -31
  132. package/dist/chunk-2HGWOLVN.mjs.map +0 -1
  133. package/dist/chunk-PFQYNPQJ.mjs.map +0 -1
  134. package/dist/chunk-VVCCMJS7.mjs.map +0 -1
  135. package/dist/chunk-ZPV43WN2.mjs.map +0 -1
  136. package/dist/cs-JTJXTX2T.mjs.map +0 -1
  137. package/dist/da-MK37SJB6.mjs.map +0 -1
  138. package/dist/de-LGBCWERA.mjs.map +0 -1
  139. package/dist/el-FKJMFCWY.mjs.map +0 -1
  140. package/dist/es-LVDPIXWU.mjs.map +0 -1
  141. package/dist/fa-3VA2PIUD.mjs.map +0 -1
  142. package/dist/fi-3WM75ZLR.mjs.map +0 -1
  143. package/dist/fr-NK4A72WA.mjs.map +0 -1
  144. package/dist/he-IACZDZMB.mjs.map +0 -1
  145. package/dist/hi-JZ7MGMMS.mjs.map +0 -1
  146. package/dist/id-P3KDQGNK.mjs.map +0 -1
  147. package/dist/it-LQS33SUY.mjs.map +0 -1
  148. package/dist/ja-G4FKZPWD.mjs.map +0 -1
  149. package/dist/ko-2XWKQ7BA.mjs.map +0 -1
  150. package/dist/ms-2SNONIUD.mjs.map +0 -1
  151. package/dist/nl-BMZUAJ7J.mjs.map +0 -1
  152. package/dist/no-6J3WIZ6L.mjs.map +0 -1
  153. package/dist/pl-QQ7DAUVK.mjs.map +0 -1
  154. package/dist/pt-MU3GN7MW.mjs.map +0 -1
  155. package/dist/ro-6GBE72QK.mjs.map +0 -1
  156. package/dist/sv-NQIL7PEM.mjs.map +0 -1
  157. package/dist/th-6OCNZQBE.mjs.map +0 -1
  158. package/dist/tr-XWJ5P3SC.mjs.map +0 -1
  159. package/dist/uk-AKSN6DGW.mjs.map +0 -1
  160. package/dist/vi-23GHQ45M.mjs.map +0 -1
  161. package/dist/zh-ITT4QBSN.mjs.map +0 -1
  162. /package/dist/{PdfAnnotationCanvas.client-PVTVPDBQ.mjs.map → PdfAnnotationCanvas.client-LF6DDTCV.mjs.map} +0 -0
  163. /package/dist/{en-AOSMPC2M.mjs.map → en-ZW4UKTVX.mjs.map} +0 -0
@@ -47,6 +47,7 @@ interface CommentsPanelProps {
47
47
  percentage?: number;
48
48
  message?: string;
49
49
  } | null;
50
+ locale?: string;
50
51
  scrollToAnnotationId?: string | null;
51
52
  onScrollCompleted?: () => void;
52
53
  hoveredAnnotationId?: string | null;
@@ -65,6 +66,7 @@ export function CommentsPanel({
65
66
  annotateMode = true,
66
67
  isAssisting = false,
67
68
  progress,
69
+ locale,
68
70
  scrollToAnnotationId,
69
71
  onScrollCompleted,
70
72
  hoveredAnnotationId,
@@ -252,6 +254,7 @@ export function CommentsPanel({
252
254
  <AssistSection
253
255
  annotationType="comment"
254
256
  isAssisting={isAssisting}
257
+ locale={locale}
255
258
  progress={progress}
256
259
  />
257
260
  )}
@@ -3,14 +3,23 @@
3
3
  import { useTranslations } from '../../../contexts/TranslationContext';
4
4
  import { useEventBus } from '../../../contexts/EventBusContext';
5
5
  import { formatLocaleDisplay } from '@semiont/api-client';
6
+ import type { components } from '@semiont/core';
6
7
  import './ResourceInfoPanel.css';
7
8
 
9
+ type Agent = components['schemas']['Agent'];
10
+
8
11
  interface Props {
9
12
  documentEntityTypes: string[];
10
13
  documentLocale?: string | undefined;
11
14
  primaryMediaType?: string | undefined;
12
15
  primaryByteSize?: number | undefined;
13
16
  isArchived?: boolean;
17
+ dateCreated?: string | undefined;
18
+ dateModified?: string | undefined;
19
+ creationMethod?: string | undefined;
20
+ wasAttributedTo?: Agent | Agent[] | undefined;
21
+ wasDerivedFrom?: string | string[] | undefined;
22
+ generator?: Agent | Agent[] | undefined;
14
23
  }
15
24
 
16
25
  /**
@@ -26,6 +35,12 @@ export function ResourceInfoPanel({
26
35
  primaryMediaType,
27
36
  primaryByteSize,
28
37
  isArchived = false,
38
+ dateCreated,
39
+ dateModified,
40
+ creationMethod,
41
+ wasAttributedTo,
42
+ wasDerivedFrom,
43
+ generator,
29
44
  }: Props) {
30
45
  const t = useTranslations('ResourceInfoPanel');
31
46
  const eventBus = useEventBus();
@@ -76,6 +91,73 @@ export function ResourceInfoPanel({
76
91
  </div>
77
92
  )}
78
93
 
94
+ {/* Provenance Section */}
95
+ {(dateCreated || dateModified || creationMethod || wasAttributedTo || wasDerivedFrom || generator) && (
96
+ <div className="semiont-resource-info-panel__section">
97
+ <h3 className="semiont-resource-info-panel__heading">{t('provenance')}</h3>
98
+ <div className="semiont-resource-info-panel__field-group">
99
+ {dateCreated && (
100
+ <div>
101
+ <span className="semiont-resource-info-panel__label">{t('createdAt')}</span>
102
+ <span className="semiont-resource-info-panel__value">
103
+ {new Date(dateCreated).toLocaleString()}
104
+ </span>
105
+ </div>
106
+ )}
107
+ {dateModified && (
108
+ <div>
109
+ <span className="semiont-resource-info-panel__label">{t('modifiedAt')}</span>
110
+ <span className="semiont-resource-info-panel__value">
111
+ {new Date(dateModified).toLocaleString()}
112
+ </span>
113
+ </div>
114
+ )}
115
+ {creationMethod && (
116
+ <div>
117
+ <span className="semiont-resource-info-panel__label">{t('creationMethod')}</span>
118
+ <span className="semiont-resource-info-panel__value">{creationMethod}</span>
119
+ </div>
120
+ )}
121
+ {wasAttributedTo && (
122
+ <div>
123
+ <span className="semiont-resource-info-panel__label">{t('attributedTo')}</span>
124
+ <span className="semiont-resource-info-panel__value">
125
+ {(Array.isArray(wasAttributedTo) ? wasAttributedTo : [wasAttributedTo])
126
+ .map(a => a.name)
127
+ .join(', ')}
128
+ </span>
129
+ </div>
130
+ )}
131
+ {wasDerivedFrom && (
132
+ <div>
133
+ <span className="semiont-resource-info-panel__label">{t('derivedFrom')}</span>
134
+ <span className="semiont-resource-info-panel__value">
135
+ {(Array.isArray(wasDerivedFrom) ? wasDerivedFrom : [wasDerivedFrom]).map((id, i) => (
136
+ <button
137
+ key={id}
138
+ className="semiont-resource-info-panel__link"
139
+ onClick={() => eventBus.get('browse:reference-navigate').next({ resourceId: id })}
140
+ >
141
+ {i > 0 && ', '}{id}
142
+ </button>
143
+ ))}
144
+ </span>
145
+ </div>
146
+ )}
147
+ {generator && (
148
+ <div>
149
+ <span className="semiont-resource-info-panel__label">{t('generatedBy')}</span>
150
+ <span className="semiont-resource-info-panel__value">
151
+ {(Array.isArray(generator) ? generator : [generator])
152
+ .map(a => a.name)
153
+ .join(', ')}
154
+ </span>
155
+ </div>
156
+ )}
157
+ </div>
158
+ </div>
159
+ )}
160
+
79
161
  {/* Entity Type Tags Section */}
80
162
  {documentEntityTypes.length > 0 && (
81
163
  <div className="semiont-resource-info-panel__section">
@@ -70,6 +70,9 @@ interface UnifiedAnnotationsPanelProps {
70
70
  // Hover coordination (for bidirectional hover highlighting)
71
71
  hoveredAnnotationId?: string | null;
72
72
 
73
+ // Locale for AI-generated text language
74
+ locale?: string;
75
+
73
76
  // Routing
74
77
  Link: React.ComponentType<LinkComponentProps>;
75
78
  routes: RouteBuilder;
@@ -238,6 +241,7 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
238
241
  isAssisting,
239
242
  progress,
240
243
  annotateMode: props.annotateMode,
244
+ locale: props.locale,
241
245
  scrollToAnnotationId: props.scrollToAnnotationId,
242
246
  onScrollCompleted: props.onScrollCompleted,
243
247
  hoveredAnnotationId: props.hoveredAnnotationId
@@ -21,6 +21,13 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
21
21
  archiveDescription: 'Move this resource to archived status',
22
22
  unarchive: 'Unarchive',
23
23
  unarchiveDescription: 'Restore this resource to active status',
24
+ provenance: 'Provenance',
25
+ createdAt: 'Created',
26
+ modifiedAt: 'Modified',
27
+ creationMethod: 'How created',
28
+ attributedTo: 'Attributed to',
29
+ derivedFrom: 'Derived from',
30
+ generatedBy: 'Generated by',
24
31
  };
25
32
  return translations[key] || key;
26
33
  }),
@@ -266,6 +273,93 @@ describe('ResourceInfoPanel Component', () => {
266
273
  });
267
274
  });
268
275
 
276
+ describe('Provenance Section', () => {
277
+ it('should not render provenance section when no provenance data', () => {
278
+ renderWithEventBus(<ResourceInfoPanel {...defaultProps} />);
279
+ expect(screen.queryByText('Provenance')).not.toBeInTheDocument();
280
+ });
281
+
282
+ it('should render dateCreated when provided', () => {
283
+ renderWithEventBus(
284
+ <ResourceInfoPanel {...defaultProps} dateCreated="2024-01-15T10:30:00Z" />
285
+ );
286
+ expect(screen.getByText('Provenance')).toBeInTheDocument();
287
+ expect(screen.getByText('Created')).toBeInTheDocument();
288
+ });
289
+
290
+ it('should render dateModified when provided', () => {
291
+ renderWithEventBus(
292
+ <ResourceInfoPanel {...defaultProps} dateModified="2024-06-01T12:00:00Z" />
293
+ );
294
+ expect(screen.getByText('Provenance')).toBeInTheDocument();
295
+ expect(screen.getByText('Modified')).toBeInTheDocument();
296
+ });
297
+
298
+ it('should render creationMethod when provided', () => {
299
+ renderWithEventBus(
300
+ <ResourceInfoPanel {...defaultProps} creationMethod="generated" />
301
+ );
302
+ expect(screen.getByText('How created')).toBeInTheDocument();
303
+ expect(screen.getByText('generated')).toBeInTheDocument();
304
+ });
305
+
306
+ it('should render a single agent attribution', () => {
307
+ renderWithEventBus(
308
+ <ResourceInfoPanel
309
+ {...defaultProps}
310
+ wasAttributedTo={{ name: 'Alice', '@id': 'https://example.org/alice' }}
311
+ />
312
+ );
313
+ expect(screen.getByText('Attributed to')).toBeInTheDocument();
314
+ expect(screen.getByText('Alice')).toBeInTheDocument();
315
+ });
316
+
317
+ it('should render multiple agent attributions', () => {
318
+ renderWithEventBus(
319
+ <ResourceInfoPanel
320
+ {...defaultProps}
321
+ wasAttributedTo={[
322
+ { name: 'Alice' },
323
+ { name: 'Bob' },
324
+ ]}
325
+ />
326
+ );
327
+ expect(screen.getByText('Alice, Bob')).toBeInTheDocument();
328
+ });
329
+
330
+ it('should render wasDerivedFrom when provided', () => {
331
+ renderWithEventBus(
332
+ <ResourceInfoPanel
333
+ {...defaultProps}
334
+ wasDerivedFrom="urn:semiont:resource:abc123"
335
+ />
336
+ );
337
+ expect(screen.getByText('Derived from')).toBeInTheDocument();
338
+ expect(screen.getByText('urn:semiont:resource:abc123')).toBeInTheDocument();
339
+ });
340
+
341
+ it('should render generator when provided', () => {
342
+ renderWithEventBus(
343
+ <ResourceInfoPanel
344
+ {...defaultProps}
345
+ generator={{ name: 'Semiont AI' }}
346
+ />
347
+ );
348
+ expect(screen.getByText('Generated by')).toBeInTheDocument();
349
+ expect(screen.getByText('Semiont AI')).toBeInTheDocument();
350
+ });
351
+
352
+ it('should render multiple generators', () => {
353
+ renderWithEventBus(
354
+ <ResourceInfoPanel
355
+ {...defaultProps}
356
+ generator={[{ name: 'Model A' }, { name: 'Model B' }]}
357
+ />
358
+ );
359
+ expect(screen.getByText('Model A, Model B')).toBeInTheDocument();
360
+ });
361
+ });
362
+
269
363
  describe('Archive Actions', () => {
270
364
  it('should render archive button when not archived', () => {
271
365
  renderWithEventBus(
@@ -103,13 +103,13 @@ function renderDetectionFlow(testUri: string) {
103
103
  // ─── Tests ────────────────────────────────────────────────────────────────────
104
104
 
105
105
  describe('Annotation creation clears pendingAnnotation', () => {
106
- let createAnnotationSpy: ReturnType<typeof vi.spyOn>;
106
+ let markAnnotationSpy: ReturnType<typeof vi.spyOn>;
107
107
 
108
108
  beforeEach(() => {
109
109
  vi.clearAllMocks();
110
110
  resetEventBusForTesting();
111
- createAnnotationSpy = vi
112
- .spyOn(SemiontApiClient.prototype, 'createAnnotation')
111
+ markAnnotationSpy = vi
112
+ .spyOn(SemiontApiClient.prototype, 'markAnnotation')
113
113
  .mockResolvedValue({ annotationId: MOCK_ANNOTATION.id } as any);
114
114
  });
115
115
 
@@ -143,7 +143,7 @@ describe('Annotation creation clears pendingAnnotation', () => {
143
143
  expect(screen.getByTestId('pending-motivation')).toHaveTextContent('none');
144
144
  });
145
145
 
146
- expect(createAnnotationSpy).toHaveBeenCalledTimes(1);
146
+ expect(markAnnotationSpy).toHaveBeenCalledTimes(1);
147
147
  });
148
148
 
149
149
  it('clears pendingAnnotation after creating an assessment (assessing)', async () => {
@@ -272,7 +272,7 @@ describe('Annotation creation clears pendingAnnotation', () => {
272
272
  });
273
273
 
274
274
  it('does NOT clear pendingAnnotation if API call fails', async () => {
275
- createAnnotationSpy.mockRejectedValueOnce(new Error('Network error'));
275
+ markAnnotationSpy.mockRejectedValueOnce(new Error('Network error'));
276
276
 
277
277
  const { emit } = renderDetectionFlow(TEST_URI);
278
278
 
@@ -319,6 +319,6 @@ describe('Annotation creation clears pendingAnnotation', () => {
319
319
  });
320
320
 
321
321
  // API should NOT have been called on cancel
322
- expect(createAnnotationSpy).not.toHaveBeenCalled();
322
+ expect(markAnnotationSpy).not.toHaveBeenCalled();
323
323
  });
324
324
  });
@@ -50,10 +50,10 @@ describe('Detection Progress Dismissal Bug', () => {
50
50
  close: vi.fn(),
51
51
  };
52
52
 
53
- vi.spyOn(SSEClient.prototype, 'annotateReferences').mockReturnValue(mockStream);
54
- vi.spyOn(SSEClient.prototype, 'annotateHighlights').mockReturnValue(mockStream);
55
- vi.spyOn(SSEClient.prototype, 'annotateComments').mockReturnValue(mockStream);
56
- vi.spyOn(SSEClient.prototype, 'annotateAssessments').mockReturnValue(mockStream);
53
+ vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue(mockStream);
54
+ vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue(mockStream);
55
+ vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue(mockStream);
56
+ vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue(mockStream);
57
57
  });
58
58
 
59
59
  afterEach(() => {
@@ -2,10 +2,10 @@
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 updateAnnotationBody API
5
+ * - bind:update-body → calls bindAnnotation API
6
6
  * - bind:update-body → emits bind:body-updated on success
7
7
  * - bind:update-body → emits bind:body-update-failed on error
8
- * - auth token passed to updateAnnotationBody
8
+ * - auth token passed to bindAnnotation
9
9
  *
10
10
  * The wizard modal (ReferenceWizardModal) handles modal state, context
11
11
  * gathering, search configuration, and result display. This test covers
@@ -21,7 +21,7 @@ import { useBindFlow } from '../../../hooks/useBindFlow';
21
21
  import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../../contexts/EventBusContext';
22
22
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
23
23
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
24
- import { SemiontApiClient } from '@semiont/api-client';
24
+ import { SSEClient } from '@semiont/api-client';
25
25
  import { resourceId, accessToken, annotationId } from '@semiont/core';
26
26
 
27
27
  // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
@@ -35,7 +35,7 @@ vi.mock('../../../components/Toast', () => ({
35
35
  }));
36
36
 
37
37
  describe('Bind Flow - Body Update Integration', () => {
38
- let updateAnnotationBodySpy: ReturnType<typeof vi.fn>;
38
+ let bindAnnotationSpy: ReturnType<typeof vi.fn>;
39
39
  const testId = resourceId('test-resource');
40
40
  const testToken = 'test-resolution-token';
41
41
  const testBaseUrl = 'http://localhost:4000';
@@ -44,8 +44,11 @@ describe('Bind Flow - Body Update Integration', () => {
44
44
  vi.clearAllMocks();
45
45
  resetEventBusForTesting();
46
46
 
47
- updateAnnotationBodySpy = vi.fn().mockResolvedValue({ success: true });
48
- vi.spyOn(SemiontApiClient.prototype, 'updateAnnotationBody').mockImplementation(updateAnnotationBodySpy);
47
+ bindAnnotationSpy = vi.fn().mockImplementation((_rId: any, annId: any, _req: any, opts: any) => {
48
+ queueMicrotask(() => opts.eventBus.get('bind:finished').next({ annotationId: annId }));
49
+ return { close: vi.fn() };
50
+ });
51
+ vi.spyOn(SSEClient.prototype, 'bindAnnotation').mockImplementation(bindAnnotationSpy as any);
49
52
  });
50
53
 
51
54
  afterEach(() => {
@@ -80,17 +83,17 @@ describe('Bind Flow - Body Update Integration', () => {
80
83
 
81
84
  // ─── bind:update-body ──────────────────────────────────────────────────
82
85
 
83
- it('bind:update-body calls updateAnnotationBody API', async () => {
86
+ it('bind:update-body calls bindAnnotation API', async () => {
84
87
  const { getEventBus } = renderBindFlow();
85
88
 
86
89
  act(() => { getEventBus().get('bind:update-body').next({
87
90
  annotationId: annotationId('ann-body-1'),
88
91
  resourceId: resourceId('linked-resource-id'),
89
- operations: [{ op: 'add', item: { id: 'linked-resource-id' } }],
92
+ operations: [{ op: 'add', item: { type: 'SpecificResource' as const, source: 'linked-resource-id' } }],
90
93
  }); });
91
94
 
92
95
  await waitFor(() => {
93
- expect(updateAnnotationBodySpy).toHaveBeenCalledTimes(1);
96
+ expect(bindAnnotationSpy).toHaveBeenCalledTimes(1);
94
97
  });
95
98
  });
96
99
 
@@ -100,14 +103,14 @@ describe('Bind Flow - Body Update Integration', () => {
100
103
  act(() => { getEventBus().get('bind:update-body').next({
101
104
  annotationId: annotationId('ann-auth'),
102
105
  resourceId: resourceId('resource-id'),
103
- operations: [{ op: 'replace', newItem: { id: 'resource-id' } }],
106
+ operations: [{ op: 'replace', newItem: { type: 'SpecificResource' as const, source: 'resource-id' } }],
104
107
  }); });
105
108
 
106
109
  await waitFor(() => {
107
- expect(updateAnnotationBodySpy).toHaveBeenCalled();
110
+ expect(bindAnnotationSpy).toHaveBeenCalled();
108
111
  });
109
112
 
110
- const callArgs = updateAnnotationBodySpy.mock.calls[0];
113
+ const callArgs = bindAnnotationSpy.mock.calls[0];
111
114
  expect(callArgs[3]).toHaveProperty('auth');
112
115
  expect(callArgs[3].auth).toBe(accessToken(testToken));
113
116
  });
@@ -121,7 +124,7 @@ describe('Bind Flow - Body Update Integration', () => {
121
124
  act(() => { getEventBus().get('bind:update-body').next({
122
125
  annotationId: annotationId('ann-success'),
123
126
  resourceId: resourceId('resource-id'),
124
- operations: [{ op: 'add', item: { id: 'resource-id' } }],
127
+ operations: [{ op: 'add', item: { type: 'SpecificResource' as const, source: 'resource-id' } }],
125
128
  }); });
126
129
 
127
130
  await waitFor(() => {
@@ -136,7 +139,10 @@ describe('Bind Flow - Body Update Integration', () => {
136
139
  });
137
140
 
138
141
  it('bind:update-body emits bind:body-update-failed on API error', async () => {
139
- updateAnnotationBodySpy.mockRejectedValue(new Error('Update failed'));
142
+ bindAnnotationSpy.mockImplementation((_rId: any, _annId: any, _req: any, opts: any) => {
143
+ queueMicrotask(() => opts.eventBus.get('bind:failed').next({ error: new Error('Update failed') }));
144
+ return { close: vi.fn() };
145
+ });
140
146
 
141
147
  const { getEventBus } = renderBindFlow();
142
148
  const bodyUpdateFailedSpy = vi.fn();
@@ -146,7 +152,7 @@ describe('Bind Flow - Body Update Integration', () => {
146
152
  act(() => { getEventBus().get('bind:update-body').next({
147
153
  annotationId: annotationId('ann-fail'),
148
154
  resourceId: resourceId('resource-id'),
149
- operations: [{ op: 'remove', item: { id: 'old-id' } }],
155
+ operations: [{ op: 'remove', item: { type: 'SpecificResource' as const, source: 'old-id' } }],
150
156
  }); });
151
157
 
152
158
  await waitFor(() => {
@@ -166,11 +172,11 @@ describe('Bind Flow - Body Update Integration', () => {
166
172
  act(() => { getEventBus().get('bind:update-body').next({
167
173
  annotationId: annotationId('ann-dedup'),
168
174
  resourceId: resourceId('resource-id'),
169
- operations: [{ op: 'add', item: { id: 'resource-id' } }],
175
+ operations: [{ op: 'add', item: { type: 'SpecificResource' as const, source: 'resource-id' } }],
170
176
  }); });
171
177
 
172
178
  await waitFor(() => {
173
- expect(updateAnnotationBodySpy).toHaveBeenCalledTimes(1);
179
+ expect(bindAnnotationSpy).toHaveBeenCalledTimes(1);
174
180
  });
175
181
  });
176
182
  });
@@ -35,10 +35,10 @@ describe('REPRODUCING BUG: Detection state not updating', () => {
35
35
  vi.clearAllMocks();
36
36
 
37
37
  // Minimal mock - SSE streams not needed for this test
38
- vi.spyOn(SSEClient.prototype, 'annotateReferences').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
39
- vi.spyOn(SSEClient.prototype, 'annotateHighlights').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
40
- vi.spyOn(SSEClient.prototype, 'annotateComments').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
41
- vi.spyOn(SSEClient.prototype, 'annotateAssessments').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
38
+ vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
39
+ vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
40
+ vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
41
+ vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
42
42
  });
43
43
 
44
44
  afterEach(() => {
@@ -54,8 +54,8 @@ const createMockSSEStream = () => {
54
54
 
55
55
  describe('Detection Flow - Feature Integration', () => {
56
56
  let mockStream: ReturnType<typeof createMockSSEStream>;
57
- let annotateReferencesSpy: any;
58
- let annotateHighlightsSpy: any;
57
+ let markReferencesSpy: any;
58
+ let markHighlightsSpy: any;
59
59
  let detectCommentsSpy: any;
60
60
 
61
61
  beforeEach(() => {
@@ -66,10 +66,10 @@ describe('Detection Flow - Feature Integration', () => {
66
66
  mockStream = createMockSSEStream();
67
67
 
68
68
  // Spy on SSEClient prototype methods
69
- annotateReferencesSpy = vi.spyOn(SSEClient.prototype, 'annotateReferences').mockReturnValue(mockStream as any);
70
- annotateHighlightsSpy = vi.spyOn(SSEClient.prototype, 'annotateHighlights').mockReturnValue(mockStream as any);
71
- detectCommentsSpy = vi.spyOn(SSEClient.prototype, 'annotateComments').mockReturnValue(mockStream as any);
72
- vi.spyOn(SSEClient.prototype, 'annotateAssessments').mockReturnValue(mockStream as any);
69
+ markReferencesSpy = vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue(mockStream as any);
70
+ markHighlightsSpy = vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue(mockStream as any);
71
+ detectCommentsSpy = vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue(mockStream as any);
72
+ vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue(mockStream as any);
73
73
  });
74
74
 
75
75
  afterEach(() => {
@@ -93,11 +93,11 @@ describe('Detection Flow - Feature Integration', () => {
93
93
  // CRITICAL ASSERTION: API called exactly once (not twice!)
94
94
  // This would FAIL if useBindFlow was called in multiple places
95
95
  await waitFor(() => {
96
- expect(annotateReferencesSpy).toHaveBeenCalledTimes(1);
96
+ expect(markReferencesSpy).toHaveBeenCalledTimes(1);
97
97
  });
98
98
 
99
99
  // Verify correct parameters (eventBus is passed but we don't need to verify its exact value)
100
- expect(annotateReferencesSpy).toHaveBeenCalledWith(
100
+ expect(markReferencesSpy).toHaveBeenCalledWith(
101
101
  testId,
102
102
  {
103
103
  entityTypes: ['Person', 'Organization'],
@@ -122,7 +122,7 @@ describe('Detection Flow - Feature Integration', () => {
122
122
 
123
123
  // Wait for stream to be created
124
124
  await waitFor(() => {
125
- expect(annotateReferencesSpy).toHaveBeenCalled();
125
+ expect(markReferencesSpy).toHaveBeenCalled();
126
126
  });
127
127
 
128
128
  // Simulate SSE progress event being emitted to EventBus (how SSE actually works now)
@@ -156,7 +156,7 @@ describe('Detection Flow - Feature Integration', () => {
156
156
  });
157
157
 
158
158
  await waitFor(() => {
159
- expect(annotateHighlightsSpy).toHaveBeenCalledTimes(1);
159
+ expect(markHighlightsSpy).toHaveBeenCalledTimes(1);
160
160
  });
161
161
 
162
162
  // First progress update via EventBus
@@ -291,8 +291,8 @@ describe('Detection Flow - Feature Integration', () => {
291
291
  });
292
292
 
293
293
  await waitFor(() => {
294
- expect(annotateHighlightsSpy).toHaveBeenCalledTimes(1);
295
- expect(annotateHighlightsSpy).toHaveBeenCalledWith(testId, {
294
+ expect(markHighlightsSpy).toHaveBeenCalledTimes(1);
295
+ expect(markHighlightsSpy).toHaveBeenCalledWith(testId, {
296
296
  instructions: 'Find important text',
297
297
  }, expect.objectContaining({ auth: undefined }));
298
298
  });
@@ -337,11 +337,11 @@ describe('Detection Flow - Feature Integration', () => {
337
337
 
338
338
  // Wait for operation to complete
339
339
  await waitFor(() => {
340
- expect(annotateReferencesSpy).toHaveBeenCalled();
340
+ expect(markReferencesSpy).toHaveBeenCalled();
341
341
  });
342
342
 
343
343
  // VERIFY: API called exactly once, even though multiple listeners exist
344
- expect(annotateReferencesSpy).toHaveBeenCalledTimes(1);
344
+ expect(markReferencesSpy).toHaveBeenCalledTimes(1);
345
345
 
346
346
  // VERIFY: Our additional listener was called (events work)
347
347
  expect(additionalListener).toHaveBeenCalledTimes(1);
@@ -91,7 +91,7 @@ useDebouncedCallback: (fn: any) => fn,
91
91
  useResourceAnnotations: () => ({
92
92
  clearNewAnnotationId: vi.fn(),
93
93
  newAnnotationIds: new Set(),
94
- createAnnotation: vi.fn(),
94
+ markAnnotation: vi.fn(),
95
95
  deleteAnnotation: vi.fn(),
96
96
  triggerSparkleAnimation: vi.fn(),
97
97
  }),
@@ -111,7 +111,7 @@ vi.mock('../../../contexts/ResourceAnnotationsContext', () => ({
111
111
  useResourceAnnotations: () => ({
112
112
  clearNewAnnotationId: vi.fn(),
113
113
  newAnnotationIds: new Set(),
114
- createAnnotation: vi.fn(),
114
+ markAnnotation: vi.fn(),
115
115
  deleteAnnotation: vi.fn(),
116
116
  triggerSparkleAnimation: vi.fn(),
117
117
  }),
@@ -64,7 +64,7 @@ describe('Generation Flow - Feature Integration', () => {
64
64
  mockStream = createMockGenerationStream();
65
65
 
66
66
  // Spy on SSEClient prototype method
67
- generateResourceSpy = vi.spyOn(SSEClient.prototype, 'yieldResourceFromAnnotation').mockReturnValue(mockStream as any);
67
+ generateResourceSpy = vi.spyOn(SSEClient.prototype, 'yieldResource').mockReturnValue(mockStream as any);
68
68
 
69
69
  // Mock callbacks
70
70
  mockShowSuccess = vi.fn();
@@ -76,7 +76,7 @@ describe('Generation Flow - Feature Integration', () => {
76
76
  vi.restoreAllMocks();
77
77
  });
78
78
 
79
- it('should call yieldResourceFromAnnotation exactly ONCE when generation starts', async () => {
79
+ it('should call yieldResource exactly ONCE when generation starts', async () => {
80
80
  const testResourceId = resourceId('test-resource');
81
81
  const testAnnotationId = annotationId('test-annotation');
82
82
 
@@ -158,10 +158,10 @@ describe('Detection Progress Flow Integration (Layer 3)', () => {
158
158
  mockStream = new MockSSEStream(eventBus);
159
159
 
160
160
  // Spy on SSEClient prototype methods to inject mock stream
161
- vi.spyOn(SSEClient.prototype, 'annotateHighlights').mockReturnValue(mockStream as any);
162
- vi.spyOn(SSEClient.prototype, 'annotateAssessments').mockReturnValue(mockStream as any);
163
- vi.spyOn(SSEClient.prototype, 'annotateComments').mockReturnValue(mockStream as any);
164
- vi.spyOn(SSEClient.prototype, 'annotateReferences').mockReturnValue(mockStream as any);
161
+ vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue(mockStream as any);
162
+ vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue(mockStream as any);
163
+ vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue(mockStream as any);
164
+ vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue(mockStream as any);
165
165
 
166
166
  mockAnnotations = [];
167
167
  });
@@ -415,12 +415,12 @@ export function ResourceViewerPage({
415
415
  // Error notification is handled by useYieldFlow
416
416
  }, []);
417
417
 
418
- const handleReferenceNavigate = useCallback(({ documentId }: { documentId: string }) => {
419
- if (routes.resource) {
420
- const path = routes.resource.replace('[resourceId]', encodeURIComponent(documentId));
418
+ const handleReferenceNavigate = useCallback(({ resourceId }: { resourceId: string }) => {
419
+ if (routes.resourceDetail) {
420
+ const path = routes.resourceDetail(resourceId);
421
421
  eventBus.get('browse:router-push').next({ path, reason: 'reference-link' });
422
422
  }
423
- }, [routes.resource]); // eventBus is stable singleton - never in deps
423
+ }, [routes.resourceDetail]); // eventBus is stable singleton - never in deps
424
424
 
425
425
  const handleEntityTypeClicked = useCallback(({ entityType }: { entityType: string }) => {
426
426
  if (routes.know) {
@@ -613,6 +613,7 @@ export function ResourceViewerPage({
613
613
  referencedBy={referencedBy}
614
614
  referencedByLoading={referencedByLoading}
615
615
  resourceId={rUri}
616
+ locale={locale}
616
617
  scrollToAnnotationId={scrollToAnnotationId}
617
618
  hoveredAnnotationId={hoveredAnnotationId}
618
619
  onScrollCompleted={onScrollCompleted}
@@ -643,6 +644,12 @@ export function ResourceViewerPage({
643
644
  primaryMediaType={primaryMediaType}
644
645
  primaryByteSize={primaryByteSize}
645
646
  isArchived={resource.archived ?? false}
647
+ dateCreated={resource.dateCreated}
648
+ dateModified={resource.dateModified}
649
+ creationMethod={resource.creationMethod}
650
+ wasAttributedTo={resource.wasAttributedTo}
651
+ wasDerivedFrom={resource.wasDerivedFrom}
652
+ generator={resource.generator as components['schemas']['Agent'] | components['schemas']['Agent'][] | undefined}
646
653
  />
647
654
  )}
648
655
 
@@ -690,15 +697,9 @@ export function ResourceViewerPage({
690
697
  configureGenerationTitle: tw('configureGenerationTitle'),
691
698
  configureSearchTitle: tw('configureSearchTitle'),
692
699
  searchResultsTitle: tw('searchResultsTitle'),
693
- annotationLabel: tw('annotationLabel'),
694
- sourceResourceLabel: tw('sourceResourceLabel'),
695
- motivationLabel: tw('motivationLabel'),
696
700
  sourceContextLabel: tw('sourceContextLabel'),
697
- entityTypesLabel: tw('entityTypesLabel'),
698
- graphContextLabel: tw('graphContextLabel'),
699
701
  connectionsLabel: tw('connectionsLabel'),
700
702
  citedByLabel: tw('citedByLabel'),
701
- siblingTypesLabel: tw('siblingTypesLabel'),
702
703
  userHintLabel: tw('userHintLabel'),
703
704
  userHintPlaceholder: tw('userHintPlaceholder'),
704
705
  loadingContext: tw('loadingContext'),
@@ -708,6 +709,7 @@ export function ResourceViewerPage({
708
709
  searching: tw('searching'),
709
710
  generate: tw('generate'),
710
711
  compose: tw('compose'),
712
+ resolutionStrategyLabel: tw('resolutionStrategyLabel'),
711
713
  back: tw('back'),
712
714
  link: tw('link'),
713
715
  score: tw('score'),