@semiont/react-ui 0.5.9 → 0.5.10

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-U4EVBZEV.js → PdfAnnotationCanvas.client-P37SXBWQ.js} +2 -2
  2. package/dist/{ar-WA47UUWA.js → ar-ZHKO4XDR.js} +45 -4
  3. package/dist/ar-ZHKO4XDR.js.map +1 -0
  4. package/dist/{bn-5ANDRIU6.js → bn-3BUDKAK5.js} +45 -4
  5. package/dist/bn-3BUDKAK5.js.map +1 -0
  6. package/dist/{chunk-WHUGODTB.js → chunk-5LFFAVDI.js} +32 -32
  7. package/dist/{chunk-YBGN4ACY.js → chunk-O23TLLXW.js} +1 -6
  8. package/dist/{chunk-GERSK2DY.js → chunk-R52YAGHK.js} +42 -1
  9. package/dist/chunk-R52YAGHK.js.map +1 -0
  10. package/dist/{cs-3RU7F4JX.js → cs-2EFEUKTW.js} +45 -4
  11. package/dist/cs-2EFEUKTW.js.map +1 -0
  12. package/dist/{da-GSW5P5HG.js → da-SUD3Q44Q.js} +45 -4
  13. package/dist/da-SUD3Q44Q.js.map +1 -0
  14. package/dist/{de-JUAUYF4Z.js → de-6IC5QRX7.js} +45 -4
  15. package/dist/de-6IC5QRX7.js.map +1 -0
  16. package/dist/{el-JNLWCKEC.js → el-Q4NAXFRC.js} +45 -4
  17. package/dist/el-Q4NAXFRC.js.map +1 -0
  18. package/dist/{en-RBN2GUHF.js → en-P2L4X27N.js} +5 -3
  19. package/dist/{es-WPOX225H.js → es-YUKZBD4W.js} +45 -4
  20. package/dist/es-YUKZBD4W.js.map +1 -0
  21. package/dist/{fa-TAEP6N77.js → fa-OJ47LILE.js} +45 -4
  22. package/dist/fa-OJ47LILE.js.map +1 -0
  23. package/dist/{fi-ZVZHANSP.js → fi-QFFL5CUB.js} +45 -4
  24. package/dist/fi-QFFL5CUB.js.map +1 -0
  25. package/dist/{fr-VLZW7M4N.js → fr-BOQZZWP4.js} +45 -4
  26. package/dist/fr-BOQZZWP4.js.map +1 -0
  27. package/dist/{he-QFAFYA77.js → he-LXDY3TFA.js} +45 -4
  28. package/dist/he-LXDY3TFA.js.map +1 -0
  29. package/dist/{hi-AO3DQPCV.js → hi-HOJER77V.js} +45 -4
  30. package/dist/hi-HOJER77V.js.map +1 -0
  31. package/dist/{id-GTXF42WM.js → id-I5WQ6EWD.js} +45 -4
  32. package/dist/id-I5WQ6EWD.js.map +1 -0
  33. package/dist/index.d.ts +119 -9
  34. package/dist/index.js +4417 -18858
  35. package/dist/index.js.map +1 -1
  36. package/dist/integrations/css-modules-helper.js +1 -1
  37. package/dist/integrations/styled-components-theme.js +1 -1
  38. package/dist/{it-AS5GM232.js → it-UYH642IJ.js} +45 -4
  39. package/dist/it-UYH642IJ.js.map +1 -0
  40. package/dist/{ja-GZ4HLUOF.js → ja-JTVWAC4L.js} +45 -4
  41. package/dist/ja-JTVWAC4L.js.map +1 -0
  42. package/dist/{ko-A4XUXFGJ.js → ko-IHXFMOIZ.js} +45 -4
  43. package/dist/ko-IHXFMOIZ.js.map +1 -0
  44. package/dist/{ms-YBAO3S6K.js → ms-JH25VFLO.js} +45 -4
  45. package/dist/ms-JH25VFLO.js.map +1 -0
  46. package/dist/{nl-3TJGIIIU.js → nl-SURXW7IQ.js} +45 -4
  47. package/dist/nl-SURXW7IQ.js.map +1 -0
  48. package/dist/{no-4AXIQPRQ.js → no-HXIKQFDZ.js} +45 -4
  49. package/dist/no-HXIKQFDZ.js.map +1 -0
  50. package/dist/{pl-5GP6GKXO.js → pl-7A4KU6XK.js} +45 -4
  51. package/dist/pl-7A4KU6XK.js.map +1 -0
  52. package/dist/{pt-26Y6JFG5.js → pt-6MGYP7Q5.js} +45 -4
  53. package/dist/pt-6MGYP7Q5.js.map +1 -0
  54. package/dist/{ro-C7UXFRWJ.js → ro-IROCHNIA.js} +45 -4
  55. package/dist/ro-IROCHNIA.js.map +1 -0
  56. package/dist/{sv-44DVD76T.js → sv-WDJD75CV.js} +45 -4
  57. package/dist/sv-WDJD75CV.js.map +1 -0
  58. package/dist/test-utils.js +3 -3
  59. package/dist/{th-GIQRTBOY.js → th-RIO2E6B3.js} +45 -4
  60. package/dist/th-RIO2E6B3.js.map +1 -0
  61. package/dist/{tr-WMQWO4D6.js → tr-TZ7ERCIN.js} +45 -4
  62. package/dist/tr-TZ7ERCIN.js.map +1 -0
  63. package/dist/{uk-I7ML6RGG.js → uk-ZFWDKLUE.js} +45 -4
  64. package/dist/uk-ZFWDKLUE.js.map +1 -0
  65. package/dist/{vi-FGWECASQ.js → vi-UIOSP2JG.js} +45 -4
  66. package/dist/vi-UIOSP2JG.js.map +1 -0
  67. package/dist/{zh-L5FN73XC.js → zh-6R6YUY46.js} +45 -4
  68. package/dist/zh-6R6YUY46.js.map +1 -0
  69. package/package.json +17 -8
  70. package/src/components/AnnotateReferencesProgressWidget.tsx +35 -16
  71. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +41 -91
  72. package/src/components/modals/ConfigureGatherStep.tsx +102 -0
  73. package/src/components/modals/ContextSummary.tsx +15 -6
  74. package/src/components/modals/GatherContextStep.tsx +104 -59
  75. package/src/components/modals/ResourceGenerateModal.tsx +258 -0
  76. package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +82 -0
  77. package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +107 -0
  78. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +197 -0
  79. package/src/components/resource/panels/ReferencesPanel.tsx +10 -0
  80. package/src/components/resource/panels/ResourceInfoPanel.tsx +22 -0
  81. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +26 -0
  82. package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +14 -0
  83. package/src/features/admin-exchange/state/exchange-state-unit.ts +4 -2
  84. package/src/features/admin-security/state/__tests__/admin-security-state-unit.test.ts +14 -0
  85. package/src/features/admin-security/state/admin-security-state-unit.ts +4 -2
  86. package/src/features/admin-users/state/__tests__/admin-users-state-unit.test.ts +12 -5
  87. package/src/features/admin-users/state/admin-users-state-unit.ts +4 -2
  88. package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +11 -0
  89. package/src/features/auth-welcome/state/welcome-state-unit.ts +1 -1
  90. package/src/features/moderate-entity-tags/state/__tests__/entity-tags-state-unit.test.ts +14 -0
  91. package/src/features/moderate-entity-tags/state/entity-tags-state-unit.ts +4 -2
  92. package/src/features/resource-compose/components/ResourceComposePage.tsx +13 -7
  93. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +13 -0
  94. package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -2
  95. package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +15 -9
  96. package/src/features/resource-discovery/state/discover-state-unit.ts +5 -2
  97. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +79 -0
  98. package/src/features/resource-viewer/state/__tests__/resource-loader-state-unit.test.ts +11 -0
  99. package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +30 -0
  100. package/src/features/resource-viewer/state/resource-loader-state-unit.ts +1 -1
  101. package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +5 -2
  102. package/translations/ar.json +41 -2
  103. package/translations/bn.json +41 -2
  104. package/translations/cs.json +41 -2
  105. package/translations/da.json +41 -2
  106. package/translations/de.json +41 -2
  107. package/translations/el.json +41 -2
  108. package/translations/en.json +39 -0
  109. package/translations/es.json +41 -2
  110. package/translations/fa.json +41 -2
  111. package/translations/fi.json +41 -2
  112. package/translations/fr.json +41 -2
  113. package/translations/he.json +41 -2
  114. package/translations/hi.json +41 -2
  115. package/translations/id.json +41 -2
  116. package/translations/it.json +41 -2
  117. package/translations/ja.json +41 -2
  118. package/translations/ko.json +41 -2
  119. package/translations/ms.json +41 -2
  120. package/translations/nl.json +41 -2
  121. package/translations/no.json +41 -2
  122. package/translations/pl.json +41 -2
  123. package/translations/pt.json +41 -2
  124. package/translations/ro.json +41 -2
  125. package/translations/sv.json +41 -2
  126. package/translations/th.json +41 -2
  127. package/translations/tr.json +41 -2
  128. package/translations/uk.json +41 -2
  129. package/translations/vi.json +41 -2
  130. package/translations/zh.json +41 -2
  131. package/dist/ar-WA47UUWA.js.map +0 -1
  132. package/dist/bn-5ANDRIU6.js.map +0 -1
  133. package/dist/chunk-GERSK2DY.js.map +0 -1
  134. package/dist/cs-3RU7F4JX.js.map +0 -1
  135. package/dist/da-GSW5P5HG.js.map +0 -1
  136. package/dist/de-JUAUYF4Z.js.map +0 -1
  137. package/dist/el-JNLWCKEC.js.map +0 -1
  138. package/dist/es-WPOX225H.js.map +0 -1
  139. package/dist/fa-TAEP6N77.js.map +0 -1
  140. package/dist/fi-ZVZHANSP.js.map +0 -1
  141. package/dist/fr-VLZW7M4N.js.map +0 -1
  142. package/dist/he-QFAFYA77.js.map +0 -1
  143. package/dist/hi-AO3DQPCV.js.map +0 -1
  144. package/dist/id-GTXF42WM.js.map +0 -1
  145. package/dist/it-AS5GM232.js.map +0 -1
  146. package/dist/ja-GZ4HLUOF.js.map +0 -1
  147. package/dist/ko-A4XUXFGJ.js.map +0 -1
  148. package/dist/ms-YBAO3S6K.js.map +0 -1
  149. package/dist/nl-3TJGIIIU.js.map +0 -1
  150. package/dist/no-4AXIQPRQ.js.map +0 -1
  151. package/dist/pl-5GP6GKXO.js.map +0 -1
  152. package/dist/pt-26Y6JFG5.js.map +0 -1
  153. package/dist/ro-C7UXFRWJ.js.map +0 -1
  154. package/dist/sv-44DVD76T.js.map +0 -1
  155. package/dist/th-GIQRTBOY.js.map +0 -1
  156. package/dist/tr-WMQWO4D6.js.map +0 -1
  157. package/dist/uk-I7ML6RGG.js.map +0 -1
  158. package/dist/vi-FGWECASQ.js.map +0 -1
  159. package/dist/zh-L5FN73XC.js.map +0 -1
  160. /package/dist/{PdfAnnotationCanvas.client-U4EVBZEV.js.map → PdfAnnotationCanvas.client-P37SXBWQ.js.map} +0 -0
  161. /package/dist/{chunk-WHUGODTB.js.map → chunk-5LFFAVDI.js.map} +0 -0
  162. /package/dist/{chunk-YBGN4ACY.js.map → chunk-O23TLLXW.js.map} +0 -0
  163. /package/dist/{en-RBN2GUHF.js.map → en-P2L4X27N.js.map} +0 -0
@@ -4,6 +4,7 @@ import { filter } from 'rxjs/operators';
4
4
  import type { SemiontClient } from '@semiont/sdk';
5
5
  import type { ShellStateUnit } from '../../../../state/shell-state-unit';
6
6
  import { createEntityTagsStateUnit } from '../entity-tags-state-unit';
7
+ import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing';
7
8
 
8
9
  function mockBrowse(): ShellStateUnit {
9
10
  return { dispose: vi.fn() } as unknown as ShellStateUnit;
@@ -100,3 +101,16 @@ describe('createEntityTagsStateUnit', () => {
100
101
  stateUnit.dispose();
101
102
  });
102
103
  });
104
+
105
+ describe('EntityTagsStateUnit — StateUnit axioms', () => {
106
+ it('satisfies the StateUnit axioms (incl. A7-passed: never disposes the injected browse)', () => {
107
+ assertStateUnitAxioms({
108
+ setup: () => {
109
+ const browse = disposeProbe();
110
+ return { unit: createEntityTagsStateUnit(mockClient(), browse as unknown as ShellStateUnit), passedIn: [browse] };
111
+ },
112
+ surfaces: (u) => [u.newTag$, u.error$, u.isAdding$],
113
+ invocations: (u) => [() => u.setNewTag(''), () => u.addTag()],
114
+ });
115
+ });
116
+ });
@@ -1,6 +1,6 @@
1
1
  import { BehaviorSubject, type Observable, map } from 'rxjs';
2
2
  import { createDisposer } from '@semiont/sdk';
3
- import type { StateUnit } from '@semiont/sdk';
3
+ import type { StateUnit } from '@semiont/core';
4
4
  import type { ShellStateUnit } from '../../../state/shell-state-unit';
5
5
  import type { SemiontClient } from '@semiont/sdk';
6
6
 
@@ -20,7 +20,9 @@ export function createEntityTagsStateUnit(
20
20
  browse: ShellStateUnit,
21
21
  ): EntityTagsStateUnit {
22
22
  const disposer = createDisposer();
23
- disposer.add(browse);
23
+ // `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
24
+ // (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
25
+ // shared, app-scoped shell). See packages/sdk/docs/STATE-UNITS.md (composition rule).
24
26
 
25
27
  const newTag$ = new BehaviorSubject<string>('');
26
28
  const error$ = new BehaviorSubject<string>('');
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import React, { useState, useEffect } from 'react';
10
- import type { GatheredContext } from '@semiont/core';
10
+ import { deriveViews, type GatheredContext } from '@semiont/core';
11
11
  import { capabilitiesOf, AUTHORABLE_MEDIA_TYPES, MEDIA_TYPES, mediaTypeForExtension, isSupportedMediaType, LOCALES } from '@semiont/core';
12
12
  import type { UploadProgress } from '@semiont/sdk';
13
13
  import { type CloneData, type ReferenceData } from '../state/compose-page-state-unit';
@@ -283,6 +283,12 @@ export function ResourceComposePage({
283
283
  const isClone = mode === 'clone';
284
284
  const isReferenceCompletion = mode === 'reference';
285
285
 
286
+ // Gathered-context panel — annotation-focus only for now.
287
+ const composeFocus = gatheredContext?.focus.kind === 'annotation' ? gatheredContext.focus : null;
288
+ const composeConnections = gatheredContext && composeFocus
289
+ ? deriveViews(gatheredContext.graph, String(composeFocus.sourceResource.id)).connections
290
+ : [];
291
+
286
292
  return (
287
293
  <div className={`semiont-page${activePanel && COMMON_PANELS.includes(activePanel as ToolbarPanelType) ? ' semiont-page--panel-open' : ''}`}>
288
294
  {/* Main Content Area */}
@@ -316,7 +322,7 @@ export function ResourceComposePage({
316
322
  border: '1px solid var(--semiont-border-primary)',
317
323
  }}>
318
324
  <h3 className="semiont-form__label" style={{ marginBottom: '0.75rem' }}>Gathered Context</h3>
319
- {gatheredContext.sourceContext && (
325
+ {composeFocus?.selected && (
320
326
  <div style={{
321
327
  padding: '0.75rem',
322
328
  backgroundColor: 'var(--semiont-bg-primary)',
@@ -327,27 +333,27 @@ export function ResourceComposePage({
327
333
  marginBottom: '0.5rem',
328
334
  }}>
329
335
  <div style={{ fontSize: 'var(--semiont-text-sm)', fontFamily: 'monospace', whiteSpace: 'pre-wrap', color: 'var(--semiont-text-secondary)' }}>
330
- {gatheredContext.sourceContext.before && <span>{gatheredContext.sourceContext.before}</span>}
336
+ {composeFocus.selected.before && <span>{composeFocus.selected.before}</span>}
331
337
  <span style={{
332
338
  backgroundColor: 'var(--semiont-color-primary-100)',
333
339
  padding: '0 0.25rem',
334
340
  fontWeight: 600,
335
341
  color: 'var(--semiont-color-primary-900)',
336
342
  }}>
337
- {gatheredContext.sourceContext.selected}
343
+ {composeFocus.selected.text}
338
344
  </span>
339
- {gatheredContext.sourceContext.after && <span>{gatheredContext.sourceContext.after}</span>}
345
+ {composeFocus.selected.after && <span>{composeFocus.selected.after}</span>}
340
346
  </div>
341
347
  </div>
342
348
  )}
343
- {gatheredContext.graphContext && gatheredContext.graphContext.connections && gatheredContext.graphContext.connections.length > 0 && (
349
+ {composeConnections.length > 0 && (
344
350
  <div style={{
345
351
  padding: '0.5rem 0',
346
352
  fontSize: 'var(--semiont-text-sm)',
347
353
  color: 'var(--semiont-text-secondary)',
348
354
  }}>
349
355
  <span style={{ fontSize: 'var(--semiont-text-xs)', fontWeight: 500, marginRight: '0.5rem' }}>Connections:</span>
350
- {gatheredContext.graphContext.connections.map(conn => (
356
+ {composeConnections.map(conn => (
351
357
  <span key={conn.resourceId} className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.5rem', marginRight: '0.25rem' }}>
352
358
  {conn.resourceName}
353
359
  </span>
@@ -4,6 +4,7 @@ import { filter } from 'rxjs/operators';
4
4
  import type { SemiontClient } from '@semiont/sdk';
5
5
  import type { ShellStateUnit } from '../../../../state/shell-state-unit';
6
6
  import { createComposePageStateUnit } from '../compose-page-state-unit';
7
+ import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing';
7
8
 
8
9
  /** Build an `UploadObservable`-shaped mock that emits started → finished. */
9
10
  function mockUpload(resourceId: string) {
@@ -185,3 +186,15 @@ describe('createComposePageStateUnit', () => {
185
186
  stateUnit.dispose();
186
187
  });
187
188
  });
189
+
190
+ describe('ComposePageStateUnit — StateUnit axioms', () => {
191
+ it('satisfies the StateUnit axioms (incl. A7-passed: never disposes the injected browse)', () => {
192
+ assertStateUnitAxioms({
193
+ setup: () => {
194
+ const browse = disposeProbe();
195
+ return { unit: createComposePageStateUnit(mockClient(), browse as unknown as ShellStateUnit, {}), passedIn: [browse] };
196
+ },
197
+ surfaces: (u) => [u.mode$, u.loading$, u.cloneData$, u.referenceData$, u.gatheredContext$, u.uploadProgress$],
198
+ });
199
+ });
200
+ });
@@ -2,7 +2,7 @@ import { BehaviorSubject, type Observable, map } from 'rxjs';
2
2
  import type { GatheredContext, AnnotationId, AccessToken, ResourceDescriptor, ResourceId } from '@semiont/core';
3
3
  import { resourceId as makeResourceId, annotationId as makeAnnotationId } from '@semiont/core';
4
4
  import { createDisposer } from '@semiont/sdk';
5
- import type { StateUnit } from '@semiont/sdk';
5
+ import type { StateUnit } from '@semiont/core';
6
6
  import type { ShellStateUnit } from '../../../state/shell-state-unit';
7
7
  import type { SemiontClient } from '@semiont/sdk';
8
8
  import { decodeWithCharset, extensionForMediaType } from '@semiont/core';
@@ -72,7 +72,9 @@ export function createComposePageStateUnit(
72
72
  auth?: AccessToken,
73
73
  ): ComposePageStateUnit {
74
74
  const disposer = createDisposer();
75
- disposer.add(browse);
75
+ // `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
76
+ // (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
77
+ // shared, app-scoped shell). See packages/sdk/docs/STATE-UNITS.md (composition rule).
76
78
 
77
79
  const isReferenceMode = Boolean(params.annotationUri && params.sourceDocumentId && params.name);
78
80
  const isCloneMode = params.mode === 'clone' && Boolean(params.token);
@@ -199,6 +201,7 @@ export function createComposePageStateUnit(
199
201
  cloneData$.complete();
200
202
  referenceData$.complete();
201
203
  gatheredContext$.complete();
204
+ uploadProgress$.complete();
202
205
  disposer.dispose();
203
206
  },
204
207
  };
@@ -4,6 +4,7 @@ import { filter, skip, take, toArray } from 'rxjs/operators';
4
4
  import type { SemiontClient } from '@semiont/sdk';
5
5
  import type { ShellStateUnit } from '../../../../state/shell-state-unit';
6
6
  import { createDiscoverStateUnit } from '../discover-state-unit';
7
+ import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing';
7
8
 
8
9
  function mockBrowse(): ShellStateUnit {
9
10
  return { dispose: vi.fn() } as unknown as ShellStateUnit;
@@ -100,15 +101,6 @@ describe('createDiscoverStateUnit', () => {
100
101
  stateUnit.dispose();
101
102
  });
102
103
 
103
- it('disposes browse and search on dispose', () => {
104
- const browse = mockBrowse();
105
- const { client } = mockClient();
106
- const stateUnit = createDiscoverStateUnit(client, browse);
107
- stateUnit.dispose();
108
-
109
- expect(browse.dispose).toHaveBeenCalled();
110
- });
111
-
112
104
  it('initial selectedEntityType$ is empty and recent fetch carries no entityType', async () => {
113
105
  const { client, resourceCalls } = mockClient();
114
106
  const stateUnit = createDiscoverStateUnit(client, mockBrowse());
@@ -267,3 +259,17 @@ describe('createDiscoverStateUnit', () => {
267
259
  stateUnit.dispose();
268
260
  });
269
261
  });
262
+
263
+ describe('DiscoverStateUnit — StateUnit axioms', () => {
264
+ it('satisfies the StateUnit axioms (incl. A7-passed: never disposes the injected browse)', () => {
265
+ assertStateUnitAxioms({
266
+ setup: () => {
267
+ const browse = disposeProbe();
268
+ const { client } = mockClient();
269
+ return { unit: createDiscoverStateUnit(client, browse as unknown as ShellStateUnit), passedIn: [browse] };
270
+ },
271
+ surfaces: (u) => [u.selectedEntityType$],
272
+ invocations: (u) => [() => u.setSelectedEntityType('')],
273
+ });
274
+ });
275
+ });
@@ -1,7 +1,8 @@
1
1
  import { BehaviorSubject, Subject, combineLatest, of, type Observable } from 'rxjs';
2
2
  import { debounceTime, distinctUntilChanged, map, startWith, switchMap, shareReplay } from 'rxjs/operators';
3
3
  import type { ResourceDescriptor } from '@semiont/core';
4
- import type { SemiontClient, StateUnit } from '@semiont/sdk';
4
+ import type { SemiontClient } from '@semiont/sdk';
5
+ import type { StateUnit } from '@semiont/core';
5
6
  import { createDisposer } from '@semiont/sdk';
6
7
  import type { ShellStateUnit } from '../../../state/shell-state-unit';
7
8
 
@@ -30,7 +31,9 @@ export function createDiscoverStateUnit(
30
31
  browse: ShellStateUnit,
31
32
  ): DiscoverStateUnit {
32
33
  const disposer = createDisposer();
33
- disposer.add(browse);
34
+ // `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
35
+ // (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
36
+ // shared, app-scoped shell). See packages/sdk/docs/STATE-UNITS.md (composition rule).
34
37
 
35
38
  // Selected entity-type chip on the Discover page. Drives both the
36
39
  // `recent` list and the search results — filtering happens on the
@@ -35,6 +35,8 @@ import { useStateUnit } from '../../../hooks/useStateUnit';
35
35
  import { useShellStateUnit } from '../../../hooks/useShellStateUnit';
36
36
  import { useTranslations } from '../../../contexts/TranslationContext';
37
37
  import { ReferenceWizardModal } from '../../../components/modals/ReferenceWizardModal';
38
+ import { ResourceGenerateModal } from '../../../components/modals/ResourceGenerateModal';
39
+ import { AnnotateReferencesProgressWidget } from '../../../components/AnnotateReferencesProgressWidget';
38
40
  import type { GenerationConfig } from '../../../components/modals/ConfigureGenerationStep';
39
41
 
40
42
  type SemiontResource = ResourceDescriptor;
@@ -128,6 +130,7 @@ export function ResourceViewerPage({
128
130
  }: ResourceViewerPageProps) {
129
131
  // Translations
130
132
  const tw = useTranslations('ReferenceWizard');
133
+ const tg = useTranslations('ResourceGenerate');
131
134
 
132
135
  const browser = useSemiont();
133
136
  const session = useObservable(browser.activeSession$);
@@ -187,6 +190,7 @@ export function ResourceViewerPage({
187
190
  const wizardResourceId = wizardState?.resourceId ?? null;
188
191
  const wizardDefaultTitle = wizardState?.defaultTitle ?? '';
189
192
  const wizardEntityTypes = wizardState?.entityTypes ?? [];
193
+ const [generateOpen, setGenerateOpen] = useState(false);
190
194
 
191
195
  const handleWizardClose = useCallback(() => {
192
196
  stateUnit.closeWizard();
@@ -209,6 +213,23 @@ export function ResourceViewerPage({
209
213
  });
210
214
  }, [stateUnit, clearNewAnnotationId, resource]);
211
215
 
216
+ // Resource-generate flow (GENERATE-FROM-BUTTON): drive the SAME yield progress$
217
+ // the annotation path uses so the full AnnotateReferencesProgressWidget shows —
218
+ // NOT a toast. `generateFromResource` is Phase 6 (the @semiont/sdk session);
219
+ // this is declared RED until that method lands. Do not re-impl it here.
220
+ const handleResourceGenerateSubmit = useCallback((_resourceId: string, config: GenerationConfig) => {
221
+ stateUnit.yield.generateFromResource({
222
+ title: config.title,
223
+ storageUri: config.storagePath,
224
+ ...(config.prompt ? { prompt: config.prompt } : {}),
225
+ language: config.language,
226
+ sourceLanguage: getLanguage(resource),
227
+ temperature: config.temperature,
228
+ maxTokens: config.maxTokens,
229
+ context: config.context,
230
+ });
231
+ }, [stateUnit, resource]);
232
+
212
233
  const handleWizardLinkResource = useCallback(async (referenceId: string, targetResourceId: string) => {
213
234
  if (!semiont) return;
214
235
  try {
@@ -452,6 +473,21 @@ export function ResourceViewerPage({
452
473
  </h2>
453
474
  </div>
454
475
  </div>
476
+ {/* Resource-generation progress (GENERATE-FROM-BUTTON P7) — no annotationId ⇒ a resource-gen job */}
477
+ {generationProgress && !generationProgress.annotationId && (
478
+ <AnnotateReferencesProgressWidget
479
+ progress={generationProgress}
480
+ annotationType="generation"
481
+ cancelJobType="generation"
482
+ translations={{
483
+ title: tg('progressTitle'),
484
+ cancel: tg('progressCancel'),
485
+ inProgress: tg('progressInProgress'),
486
+ complete: tg('progressComplete'),
487
+ failed: tg('progressFailed'),
488
+ }}
489
+ />
490
+ )}
455
491
  {/* Scrollable body wrapper - contains document content, header is sibling above */}
456
492
  <div className="semiont-document-viewer__scrollable-body" lang={getLanguage(resource) || undefined}>
457
493
  <ErrorBoundary
@@ -565,6 +601,7 @@ export function ResourceViewerPage({
565
601
  wasAttributedTo={resource.wasAttributedTo}
566
602
  wasDerivedFrom={resource.wasDerivedFrom}
567
603
  generator={resource.generator as components['schemas']['Agent'] | components['schemas']['Agent'][] | undefined}
604
+ onGenerate={() => setGenerateOpen(true)}
568
605
  />
569
606
  )}
570
607
 
@@ -645,6 +682,48 @@ export function ResourceViewerPage({
645
682
  semanticScoringHelp: tw('semanticScoringHelp'),
646
683
  }}
647
684
  />
685
+
686
+ {/* Resource-generate flow (GENERATE-FROM-BUTTON) */}
687
+ <ResourceGenerateModal
688
+ isOpen={generateOpen}
689
+ onClose={() => setGenerateOpen(false)}
690
+ resourceId={rUri}
691
+ defaultTitle=""
692
+ locale={locale}
693
+ onGenerateSubmit={handleResourceGenerateSubmit}
694
+ translations={{
695
+ gatherTitle: tg('gatherTitle'),
696
+ reviewTitle: tg('reviewTitle'),
697
+ configureTitle: tg('configureTitle'),
698
+ next: tg('next'),
699
+ back: tg('back'),
700
+ cancel: tg('cancel'),
701
+ gatherIntro: tg('gatherIntro'),
702
+ includeContent: tg('includeContent'),
703
+ includeSummary: tg('includeSummary'),
704
+ gatherDepth: tg('gatherDepth'),
705
+ gatherMaxResources: tg('gatherMaxResources'),
706
+ gatherButton: tg('gatherButton'),
707
+ excludeLabel: tg('excludeLabel'),
708
+ loadingContext: tg('loadingContext'),
709
+ failedContext: tg('failedContext'),
710
+ sourceContextLabel: tg('sourceContextLabel'),
711
+ connectionsLabel: tg('connectionsLabel'),
712
+ citedByLabel: tg('citedByLabel'),
713
+ resourceTitle: tg('resourceTitle'),
714
+ resourceTitlePlaceholder: tg('resourceTitlePlaceholder'),
715
+ additionalInstructions: tg('additionalInstructions'),
716
+ additionalInstructionsPlaceholder: tg('additionalInstructionsPlaceholder'),
717
+ language: tg('language'),
718
+ languageHelp: tg('languageHelp'),
719
+ creativity: tg('creativity'),
720
+ creativityFocused: tg('creativityFocused'),
721
+ creativityCreative: tg('creativityCreative'),
722
+ maxLength: tg('maxLength'),
723
+ maxLengthHelp: tg('maxLengthHelp'),
724
+ generate: tg('generate'),
725
+ }}
726
+ />
648
727
  </div>
649
728
  );
650
729
  }
@@ -4,6 +4,7 @@ import { filter } from 'rxjs/operators';
4
4
  import { resourceId as makeResourceId } from '@semiont/core';
5
5
  import type { SemiontClient } from '@semiont/sdk';
6
6
  import { createResourceLoaderStateUnit } from '../resource-loader-state-unit';
7
+ import { assertStateUnitAxioms } from '@semiont/core/testing';
7
8
 
8
9
  const RID = makeResourceId('res-1');
9
10
 
@@ -44,3 +45,13 @@ describe('createResourceLoaderStateUnit', () => {
44
45
  stateUnit.dispose();
45
46
  });
46
47
  });
48
+
49
+ describe('ResourceLoaderStateUnit — StateUnit axioms', () => {
50
+ it('satisfies the StateUnit axioms', () => {
51
+ // No owned surfaces: resource$/isLoading$ are derived from client.browse; dispose is a no-op.
52
+ assertStateUnitAxioms({
53
+ setup: () => createResourceLoaderStateUnit(mockClient(), RID),
54
+ invocations: (u) => [() => u.invalidate()],
55
+ });
56
+ });
57
+ });
@@ -4,6 +4,7 @@ import { filter } from 'rxjs/operators';
4
4
  import { annotationId, resourceId as makeResourceId } from '@semiont/core';
5
5
  import type { ShellStateUnit } from '../../../../state/shell-state-unit';
6
6
  import { createResourceViewerPageStateUnit } from '../resource-viewer-page-state-unit';
7
+ import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing';
7
8
  import { makeTestClient, type TestClient } from '../../../../__tests__/test-client';
8
9
 
9
10
  const RID = makeResourceId('res-1');
@@ -237,3 +238,32 @@ describe('createResourceViewerPageStateUnit', () => {
237
238
  stateUnit.dispose();
238
239
  });
239
240
  });
241
+
242
+ describe('ResourceViewerPageStateUnit — StateUnit axioms', () => {
243
+ it('satisfies the StateUnit axioms (A7-passed: browse survives; A7-owned: children disposed)', () => {
244
+ const RID = makeResourceId('res-ax');
245
+ assertStateUnitAxioms({
246
+ setup: () => {
247
+ const browse = disposeProbe();
248
+ const tc = clientWithNamespaces();
249
+ return {
250
+ unit: createResourceViewerPageStateUnit(tc.client, RID, 'en', browse as unknown as ShellStateUnit),
251
+ passedIn: [browse],
252
+ teardown: () => tc.bus.destroy(),
253
+ };
254
+ },
255
+ surfaces: (u) => [u.wizard$, u.content$, u.contentLoading$, u.mediaToken$],
256
+ invocations: (u) => [() => u.closeWizard()],
257
+ // A7-owned: the internally-constructed children must be disposed when the page
258
+ // disposes — proven by their own surfaces completing. (Match has no public
259
+ // surface; it's disposed via the same disposer.)
260
+ ownedChildSurfaces: (u) => [
261
+ u.beckon.hoveredAnnotationId$,
262
+ u.mark.pendingAnnotation$,
263
+ u.gather.context$,
264
+ u.yield.isGenerating$,
265
+ ],
266
+ numRuns: 5,
267
+ });
268
+ });
269
+ });
@@ -1,6 +1,6 @@
1
1
  import { type Observable, map } from 'rxjs';
2
2
  import type { ResourceDescriptor, ResourceId } from '@semiont/core';
3
- import type { StateUnit } from '@semiont/sdk';
3
+ import type { StateUnit } from '@semiont/core';
4
4
  import type { SemiontClient } from '@semiont/sdk';
5
5
 
6
6
  export interface ResourceLoaderStateUnit extends StateUnit {
@@ -1,7 +1,7 @@
1
1
  import { BehaviorSubject, type Observable, map } from 'rxjs';
2
2
  import type { ResourceId, components } from '@semiont/core';
3
3
  import { createDisposer } from '@semiont/sdk';
4
- import type { StateUnit } from '@semiont/sdk';
4
+ import type { StateUnit } from '@semiont/core';
5
5
  import type { ShellStateUnit } from '../../../state/shell-state-unit';
6
6
  import { createBeckonStateUnit, type BeckonStateUnit } from '@semiont/sdk';
7
7
  import { createMarkStateUnit, type MarkStateUnit } from '@semiont/sdk';
@@ -72,7 +72,10 @@ export function createResourceViewerPageStateUnit(
72
72
  const yieldStateUnit = createYieldStateUnit(client, resourceId, locale);
73
73
 
74
74
  disposer.add(beckon);
75
- disposer.add(browse);
75
+ // `browse` (ShellStateUnit) is a *passed-in* dependency — owned by `useShellStateUnit`,
76
+ // not this page unit. Do NOT add it to the disposer: it's app-scoped and shared, so
77
+ // disposing it on page teardown would tear down (or double-dispose) the shared shell.
78
+ // See packages/sdk/docs/STATE-UNITS.md (composition: only dispose children you construct).
76
79
  disposer.add(mark);
77
80
  disposer.add(gather);
78
81
  disposer.add(matchStateUnit);
@@ -16,8 +16,11 @@
16
16
  "representation": "التمثيل",
17
17
  "mediaType": "نوع الوسائط",
18
18
  "byteSize": "الحجم",
19
+ "storageUri": "التخزين",
19
20
  "clone": "استنساخ",
20
21
  "cloneDescription": "إنشاء رابط استنساخ قابل للمشاركة لهذا المورد",
22
+ "generate": "توليد",
23
+ "generateDescription": "إنشاء مورد جديد من سياق هذا المورد",
21
24
  "archive": "أرشفة",
22
25
  "archiveDescription": "نقل هذا المورد إلى حالة الأرشيف",
23
26
  "unarchive": "إلغاء الأرشفة",
@@ -26,8 +29,7 @@
26
29
  "createdAt": "تاريخ الإنشاء",
27
30
  "modifiedAt": "تاريخ التعديل",
28
31
  "attributedTo": "منسوب إلى",
29
- "derivedFrom": "مشتق من",
30
- "storageUri": "التخزين"
32
+ "derivedFrom": "مشتق من"
31
33
  },
32
34
  "CollaborationPanel": {
33
35
  "title": "التعاون",
@@ -321,6 +323,43 @@
321
323
  "hoverDelayDescription": "تأخير {{delay}} ميلي ثانية قبل ظهور تأثيرات التمرير"
322
324
  },
323
325
  "AnnotateView": {},
326
+ "ResourceGenerate": {
327
+ "gatherTitle": "تكوين الجمع",
328
+ "reviewTitle": "مراجعة السياق",
329
+ "configureTitle": "تكوين التوليد",
330
+ "next": "التالي",
331
+ "back": "رجوع",
332
+ "cancel": "إلغاء",
333
+ "gatherIntro": "اختر ما تريد تضمينه في السياق المُجمَّع.",
334
+ "includeContent": "تضمين محتوى المورد",
335
+ "includeSummary": "تضمين الملخص",
336
+ "gatherDepth": "العمق",
337
+ "gatherMaxResources": "الحد الأقصى للموارد",
338
+ "gatherButton": "جمع",
339
+ "excludeLabel": "استبعاد من الاسترجاع",
340
+ "loadingContext": "جارٍ جمع السياق...",
341
+ "failedContext": "فشل في جمع السياق",
342
+ "sourceContextLabel": "المورد",
343
+ "connectionsLabel": "الاتصالات",
344
+ "citedByLabel": "مُستشهد به من قِبل",
345
+ "resourceTitle": "عنوان المورد الجديد",
346
+ "resourceTitlePlaceholder": "عنوان المورد المُولَّد...",
347
+ "additionalInstructions": "تعليمات إضافية",
348
+ "additionalInstructionsPlaceholder": "إرشادات اختيارية للتوليد...",
349
+ "language": "اللغة",
350
+ "languageHelp": "لغة المورد المُولَّد",
351
+ "creativity": "الإبداع",
352
+ "creativityFocused": "مركّز",
353
+ "creativityCreative": "إبداعي",
354
+ "maxLength": "الحد الأقصى للطول",
355
+ "maxLengthHelp": "العدد الأقصى من الرموز",
356
+ "generate": "توليد",
357
+ "progressTitle": "جارٍ توليد المورد",
358
+ "progressCancel": "إلغاء التوليد",
359
+ "progressInProgress": "جارٍ التوليد…",
360
+ "progressComplete": "اكتمل التوليد!",
361
+ "progressFailed": "فشل التوليد"
362
+ },
324
363
  "ReferenceWizard": {
325
364
  "gatherTitle": "السياق المُجمَّع",
326
365
  "configureGenerationTitle": "تكوين التوليد",
@@ -16,8 +16,11 @@
16
16
  "representation": "উপস্থাপনা",
17
17
  "mediaType": "মিডিয়ার ধরন",
18
18
  "byteSize": "আকার",
19
+ "storageUri": "সঞ্চয়স্থান",
19
20
  "clone": "ক্লোন",
20
21
  "cloneDescription": "এই সম্পদের জন্য একটি শেয়ারযোগ্য ক্লোন লিঙ্ক তৈরি করুন",
22
+ "generate": "তৈরি করুন",
23
+ "generateDescription": "এই সম্পদের প্রসঙ্গ থেকে একটি নতুন সম্পদ তৈরি করুন",
21
24
  "archive": "সংরক্ষণাগারভুক্ত করুন",
22
25
  "archiveDescription": "এই সম্পদটিকে সংরক্ষণাগারভুক্ত অবস্থায় স্থানান্তর করুন",
23
26
  "unarchive": "সংরক্ষণাগার থেকে পুনরুদ্ধার করুন",
@@ -26,8 +29,7 @@
26
29
  "createdAt": "তৈরির তারিখ",
27
30
  "modifiedAt": "পরিবর্তনের তারিখ",
28
31
  "attributedTo": "সম্পাদককৃত",
29
- "derivedFrom": "থেকে উৎপন্ন",
30
- "storageUri": "সঞ্চয়স্থান"
32
+ "derivedFrom": "থেকে উৎপন্ন"
31
33
  },
32
34
  "CollaborationPanel": {
33
35
  "title": "সহযোগিতা",
@@ -321,6 +323,43 @@
321
323
  "hoverDelayDescription": "হোভার প্রভাব প্রদর্শনের আগে {{delay}}মি.সে. বিলম্ব"
322
324
  },
323
325
  "AnnotateView": {},
326
+ "ResourceGenerate": {
327
+ "gatherTitle": "সংগ্রহ কনফিগার করুন",
328
+ "reviewTitle": "প্রসঙ্গ পর্যালোচনা করুন",
329
+ "configureTitle": "জেনারেশন কনফিগার করুন",
330
+ "next": "পরবর্তী",
331
+ "back": "পিছনে",
332
+ "cancel": "বাতিল করুন",
333
+ "gatherIntro": "সংগৃহীত প্রসঙ্গে কী অন্তর্ভুক্ত করবেন তা বেছে নিন।",
334
+ "includeContent": "সম্পদের বিষয়বস্তু অন্তর্ভুক্ত করুন",
335
+ "includeSummary": "সারসংক্ষেপ অন্তর্ভুক্ত করুন",
336
+ "gatherDepth": "গভীরতা",
337
+ "gatherMaxResources": "সর্বোচ্চ সম্পদ",
338
+ "gatherButton": "সংগ্রহ করুন",
339
+ "excludeLabel": "পুনরুদ্ধার থেকে বাদ দিন",
340
+ "loadingContext": "প্রসঙ্গ সংগ্রহ হচ্ছে...",
341
+ "failedContext": "প্রসঙ্গ সংগ্রহ ব্যর্থ",
342
+ "sourceContextLabel": "সম্পদ",
343
+ "connectionsLabel": "সংযোগসমূহ",
344
+ "citedByLabel": "উদ্ধৃতকারী",
345
+ "resourceTitle": "নতুন সম্পদের শিরোনাম",
346
+ "resourceTitlePlaceholder": "তৈরি করা সম্পদের শিরোনাম...",
347
+ "additionalInstructions": "অতিরিক্ত নির্দেশনা",
348
+ "additionalInstructionsPlaceholder": "তৈরির জন্য ঐচ্ছিক নির্দেশনা...",
349
+ "language": "ভাষা",
350
+ "languageHelp": "তৈরি করা সম্পদের ভাষা",
351
+ "creativity": "সৃজনশীলতা",
352
+ "creativityFocused": "কেন্দ্রীভূত",
353
+ "creativityCreative": "সৃজনশীল",
354
+ "maxLength": "সর্বোচ্চ দৈর্ঘ্য",
355
+ "maxLengthHelp": "সর্বোচ্চ টোকেন সংখ্যা",
356
+ "generate": "তৈরি করুন",
357
+ "progressTitle": "সম্পদ তৈরি হচ্ছে",
358
+ "progressCancel": "তৈরি বাতিল করুন",
359
+ "progressInProgress": "তৈরি হচ্ছে…",
360
+ "progressComplete": "তৈরি সম্পূর্ণ!",
361
+ "progressFailed": "তৈরি ব্যর্থ"
362
+ },
324
363
  "ReferenceWizard": {
325
364
  "gatherTitle": "সংগৃহীত প্রসঙ্গ",
326
365
  "configureGenerationTitle": "জেনারেশন কনফিগার করুন",
@@ -16,8 +16,11 @@
16
16
  "representation": "Reprezentace",
17
17
  "mediaType": "Typ média",
18
18
  "byteSize": "Velikost",
19
+ "storageUri": "Úložiště",
19
20
  "clone": "Klonovat",
20
21
  "cloneDescription": "Vytvořit sdílitelný odkaz na klon tohoto zdroje",
22
+ "generate": "Generovat",
23
+ "generateDescription": "Vygenerovat nový zdroj z kontextu tohoto zdroje",
21
24
  "archive": "Archivovat",
22
25
  "archiveDescription": "Přesunout tento zdroj do archivovaného stavu",
23
26
  "unarchive": "Obnovit z archivu",
@@ -26,8 +29,7 @@
26
29
  "createdAt": "Vytvořeno",
27
30
  "modifiedAt": "Upraveno",
28
31
  "attributedTo": "Připsáno",
29
- "derivedFrom": "Odvozeno z",
30
- "storageUri": "Úložiště"
32
+ "derivedFrom": "Odvozeno z"
31
33
  },
32
34
  "CollaborationPanel": {
33
35
  "title": "Spolupráce",
@@ -321,6 +323,43 @@
321
323
  "hoverDelayDescription": "{{delay}}ms zpoždění před zobrazením efektů při najetí"
322
324
  },
323
325
  "AnnotateView": {},
326
+ "ResourceGenerate": {
327
+ "gatherTitle": "Konfigurovat shromáždění",
328
+ "reviewTitle": "Zkontrolovat kontext",
329
+ "configureTitle": "Konfigurovat generování",
330
+ "next": "Další",
331
+ "back": "Zpět",
332
+ "cancel": "Zrušit",
333
+ "gatherIntro": "Vyberte, co zahrnout do shromážděného kontextu.",
334
+ "includeContent": "Zahrnout obsah zdroje",
335
+ "includeSummary": "Zahrnout souhrn",
336
+ "gatherDepth": "Hloubka",
337
+ "gatherMaxResources": "Maximální počet zdrojů",
338
+ "gatherButton": "Shromáždit",
339
+ "excludeLabel": "Vyloučit z vyvolání",
340
+ "loadingContext": "Shromažďování kontextu...",
341
+ "failedContext": "Nepodařilo se shromáždit kontext",
342
+ "sourceContextLabel": "Zdroj",
343
+ "connectionsLabel": "Připojení",
344
+ "citedByLabel": "Citováno",
345
+ "resourceTitle": "Název nového zdroje",
346
+ "resourceTitlePlaceholder": "Název generovaného zdroje...",
347
+ "additionalInstructions": "Další pokyny",
348
+ "additionalInstructionsPlaceholder": "Volitelné pokyny pro generování...",
349
+ "language": "Jazyk",
350
+ "languageHelp": "Jazyk generovaného zdroje",
351
+ "creativity": "Kreativita",
352
+ "creativityFocused": "Zaměřená",
353
+ "creativityCreative": "Kreativní",
354
+ "maxLength": "Maximální délka",
355
+ "maxLengthHelp": "Maximální počet tokenů",
356
+ "generate": "Generovat",
357
+ "progressTitle": "Generování zdroje",
358
+ "progressCancel": "Zrušit generování",
359
+ "progressInProgress": "Generování…",
360
+ "progressComplete": "Generování dokončeno!",
361
+ "progressFailed": "Generování selhalo"
362
+ },
324
363
  "ReferenceWizard": {
325
364
  "gatherTitle": "Shromážděný kontext",
326
365
  "configureGenerationTitle": "Konfigurovat generování",