@semiont/react-ui 0.5.27 → 0.5.29

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 (192) hide show
  1. package/README.md +3 -3
  2. package/dist/{PdfAnnotationCanvas.client-EVUPTUWZ.js → PdfAnnotationCanvas.client-LSH3PZPY.js} +32 -5
  3. package/dist/{ar-OU3DQGC6.js → ar-AZBRADEB.js} +43 -23
  4. package/dist/{bn-PHWCR6AZ.js → bn-CCEW6AOS.js} +43 -23
  5. package/dist/{chunk-VXASXU4K.js → chunk-3ZBKCWUZ.js} +1 -1
  6. package/dist/{chunk-5VDTWRXJ.js → chunk-G4VHL4UA.js} +31 -31
  7. package/dist/{chunk-PCRRBU7H.js → chunk-SDKFZLEX.js} +43 -23
  8. package/dist/{cs-NEXYZ62I.js → cs-HXPF2CTE.js} +43 -23
  9. package/dist/{da-FAT43YCU.js → da-K5NTK7LH.js} +43 -23
  10. package/dist/{de-KRQ3WBCU.js → de-GMRQEDZI.js} +43 -23
  11. package/dist/{el-Q4IU3EKC.js → el-EUB4CI4G.js} +43 -23
  12. package/dist/{en-2KADE7N4.js → en-R5XA26NF.js} +2 -2
  13. package/dist/{es-2IO3R2MK.js → es-Z32DAZ6W.js} +43 -23
  14. package/dist/{fa-Q5GOQFXX.js → fa-G7HN6B32.js} +43 -23
  15. package/dist/{fi-R6NMRWBE.js → fi-3YBLZO7I.js} +43 -23
  16. package/dist/{fr-QY4D5E6J.js → fr-VUBH7BBS.js} +43 -23
  17. package/dist/{he-X6Y77XHY.js → he-WQQEY26B.js} +43 -23
  18. package/dist/{hi-XL7IOEQM.js → hi-HJZ6W4JB.js} +43 -23
  19. package/dist/{id-QZ4ZZS67.js → id-ZOGZ37MI.js} +43 -23
  20. package/dist/index.css +279 -74
  21. package/dist/index.d.ts +318 -393
  22. package/dist/index.js +2619 -2687
  23. package/dist/{it-ME2Q7OB6.js → it-DT6UW66F.js} +43 -23
  24. package/dist/{ja-HX7QASRI.js → ja-RUKQCOLO.js} +43 -23
  25. package/dist/{ko-EQO5AHAV.js → ko-ZH5ADKZM.js} +43 -23
  26. package/dist/{ms-UILFSF3I.js → ms-Y2AFPZTU.js} +43 -23
  27. package/dist/{nl-NNI45PMK.js → nl-OQEWIXNA.js} +43 -23
  28. package/dist/{no-MAL5F6P6.js → no-KPAYJZWY.js} +43 -23
  29. package/dist/{pl-4RMY6JAJ.js → pl-WSXK2UUD.js} +43 -23
  30. package/dist/{pt-VQH2X3QQ.js → pt-NWRJRUCK.js} +43 -23
  31. package/dist/{ro-XXCQO2OG.js → ro-3CGVMOC6.js} +43 -23
  32. package/dist/{sv-7Z6EJ6U4.js → sv-HGSEBA7S.js} +43 -23
  33. package/dist/test-utils.js +16 -7
  34. package/dist/{th-L3DXXYVQ.js → th-NZ4G352Z.js} +43 -23
  35. package/dist/{tr-P3DFVLFJ.js → tr-LGGS4DLW.js} +43 -23
  36. package/dist/{uk-7C5D3PXS.js → uk-AIX536L4.js} +43 -23
  37. package/dist/{vi-ZLTLEO7P.js → vi-V6CVQZLQ.js} +43 -23
  38. package/dist/{zh-5FHVWDIN.js → zh-AGKPGI3L.js} +43 -23
  39. package/package.json +16 -15
  40. package/src/components/AssistProgress.tsx +36 -2
  41. package/src/components/CodeMirrorRenderer.tsx +8 -8
  42. package/src/components/StatusDisplay.tsx +13 -13
  43. package/src/components/__tests__/AssistProgress.test.tsx +59 -0
  44. package/src/components/__tests__/StatusDisplay.test.tsx +9 -9
  45. package/src/components/modals/ComposeStep.tsx +233 -0
  46. package/src/components/modals/ConfigureGatherStep.tsx +10 -18
  47. package/src/components/modals/ConfigureGenerationStep.tsx +178 -34
  48. package/src/components/modals/ConfigureSearchStep.tsx +19 -0
  49. package/src/components/modals/ContextSummary.tsx +185 -72
  50. package/src/components/modals/CorpusPane.tsx +75 -0
  51. package/src/components/modals/DiscardPrompt.tsx +38 -0
  52. package/src/components/modals/GatherContextStep.tsx +237 -107
  53. package/src/components/modals/ReferenceWizardModal.tsx +252 -59
  54. package/src/components/modals/ResourceGenerateModal.tsx +225 -112
  55. package/src/components/modals/SearchModal.css +337 -101
  56. package/src/components/modals/SearchResultsStep.tsx +61 -68
  57. package/src/components/modals/WizardFooter.tsx +7 -1
  58. package/src/components/modals/__tests__/ComposeStep.test.tsx +152 -0
  59. package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +14 -26
  60. package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +206 -8
  61. package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +24 -1
  62. package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +463 -25
  63. package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +442 -39
  64. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +300 -86
  65. package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
  66. package/src/components/modals/__tests__/WizardFooter.test.tsx +116 -9
  67. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +34 -1
  68. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +38 -1
  69. package/src/components/resource/AnnotateView.tsx +3 -3
  70. package/src/components/resource/BrowseView.tsx +8 -8
  71. package/src/components/resource/DownloadFileLink.tsx +1 -1
  72. package/src/components/resource/ResourceViewer.tsx +4 -4
  73. package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +1 -1
  74. package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +1 -1
  75. package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +1 -1
  76. package/src/components/resource/__tests__/BrowseView.test.tsx +11 -11
  77. package/src/components/resource/__tests__/download-link.test.tsx +6 -6
  78. package/src/components/resource/annotate-renderers.tsx +3 -3
  79. package/src/components/resource/panels/AssistSection.css +35 -0
  80. package/src/components/resource/panels/ReferenceEntry.tsx +11 -4
  81. package/src/components/resource/panels/ReferencesPanel.tsx +9 -1
  82. package/src/components/resource/panels/ResourceInfoPanel.tsx +89 -20
  83. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +7 -2
  84. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +41 -0
  85. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +1 -1
  86. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +84 -3
  87. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +149 -7
  88. package/src/features/auth/__tests__/SignInForm.a11y.test.tsx +12 -12
  89. package/src/features/auth/components/SignInForm.tsx +53 -53
  90. package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +2 -2
  91. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +0 -65
  92. package/src/features/resource-compose/components/ResourceComposePage.tsx +35 -133
  93. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +1 -100
  94. package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -57
  95. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +1 -1
  96. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +2 -2
  97. package/src/features/resource-discovery/state/discover-state-unit.ts +1 -1
  98. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +73 -17
  99. package/src/features/resource-viewer/__tests__/generation-options.test.ts +53 -0
  100. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +148 -83
  101. package/src/features/resource-viewer/generation-options.ts +29 -0
  102. package/src/styles/core/forms.css +35 -8
  103. package/src/styles/features/resource-viewer.css +0 -22
  104. package/src/styles/features/resource.css +0 -4
  105. package/src/styles/index.css +0 -1
  106. package/translations/ar.json +42 -22
  107. package/translations/bn.json +42 -22
  108. package/translations/cs.json +42 -22
  109. package/translations/da.json +42 -22
  110. package/translations/de.json +42 -22
  111. package/translations/el.json +42 -22
  112. package/translations/en.json +42 -22
  113. package/translations/es.json +42 -22
  114. package/translations/fa.json +42 -22
  115. package/translations/fi.json +42 -22
  116. package/translations/fr.json +42 -22
  117. package/translations/he.json +42 -22
  118. package/translations/hi.json +42 -22
  119. package/translations/id.json +42 -22
  120. package/translations/it.json +42 -22
  121. package/translations/ja.json +42 -22
  122. package/translations/ko.json +42 -22
  123. package/translations/ms.json +42 -22
  124. package/translations/nl.json +42 -22
  125. package/translations/no.json +42 -22
  126. package/translations/pl.json +42 -22
  127. package/translations/pt.json +42 -22
  128. package/translations/ro.json +42 -22
  129. package/translations/sv.json +42 -22
  130. package/translations/th.json +42 -22
  131. package/translations/tr.json +42 -22
  132. package/translations/uk.json +42 -22
  133. package/translations/vi.json +42 -22
  134. package/translations/zh.json +42 -22
  135. package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js.map +0 -1
  136. package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css.map +0 -1
  137. package/dist/ar-OU3DQGC6.js.map +0 -1
  138. package/dist/bn-PHWCR6AZ.js.map +0 -1
  139. package/dist/chunk-5VDTWRXJ.js.map +0 -1
  140. package/dist/chunk-BTUG3VJJ.js.map +0 -1
  141. package/dist/chunk-O23TLLXW.js.map +0 -1
  142. package/dist/chunk-PCRRBU7H.js.map +0 -1
  143. package/dist/chunk-QT7LYM72.js.map +0 -1
  144. package/dist/chunk-VXASXU4K.js.map +0 -1
  145. package/dist/cs-NEXYZ62I.js.map +0 -1
  146. package/dist/da-FAT43YCU.js.map +0 -1
  147. package/dist/de-KRQ3WBCU.js.map +0 -1
  148. package/dist/el-Q4IU3EKC.js.map +0 -1
  149. package/dist/en-2KADE7N4.js.map +0 -1
  150. package/dist/es-2IO3R2MK.js.map +0 -1
  151. package/dist/fa-Q5GOQFXX.js.map +0 -1
  152. package/dist/fi-R6NMRWBE.js.map +0 -1
  153. package/dist/fr-QY4D5E6J.js.map +0 -1
  154. package/dist/he-X6Y77XHY.js.map +0 -1
  155. package/dist/hi-XL7IOEQM.js.map +0 -1
  156. package/dist/id-QZ4ZZS67.js.map +0 -1
  157. package/dist/index.css.map +0 -1
  158. package/dist/index.js.map +0 -1
  159. package/dist/integrations/css-modules-helper.js.map +0 -1
  160. package/dist/integrations/styled-components-theme.js.map +0 -1
  161. package/dist/it-ME2Q7OB6.js.map +0 -1
  162. package/dist/ja-HX7QASRI.js.map +0 -1
  163. package/dist/ko-EQO5AHAV.js.map +0 -1
  164. package/dist/ms-UILFSF3I.js.map +0 -1
  165. package/dist/nl-NNI45PMK.js.map +0 -1
  166. package/dist/no-MAL5F6P6.js.map +0 -1
  167. package/dist/pl-4RMY6JAJ.js.map +0 -1
  168. package/dist/pt-VQH2X3QQ.js.map +0 -1
  169. package/dist/ro-XXCQO2OG.js.map +0 -1
  170. package/dist/sv-7Z6EJ6U4.js.map +0 -1
  171. package/dist/test-utils.css.map +0 -1
  172. package/dist/test-utils.js.map +0 -1
  173. package/dist/th-L3DXXYVQ.js.map +0 -1
  174. package/dist/tr-P3DFVLFJ.js.map +0 -1
  175. package/dist/uk-7C5D3PXS.js.map +0 -1
  176. package/dist/vi-ZLTLEO7P.js.map +0 -1
  177. package/dist/zh-5FHVWDIN.js.map +0 -1
  178. package/src/components/modals/ResourceSearchModal.tsx +0 -224
  179. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +0 -163
  180. package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +0 -137
  181. package/src/features/admin-exchange/__tests__/ExportCard.test.tsx +0 -41
  182. package/src/features/admin-exchange/__tests__/ImportCard.test.tsx +0 -148
  183. package/src/features/admin-exchange/__tests__/ImportProgress.test.tsx +0 -117
  184. package/src/features/admin-exchange/components/AdminExchangePage.tsx +0 -117
  185. package/src/features/admin-exchange/components/ExportCard.tsx +0 -35
  186. package/src/features/admin-exchange/components/ImportCard.tsx +0 -183
  187. package/src/features/admin-exchange/components/ImportProgress.tsx +0 -80
  188. package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +0 -185
  189. package/src/features/admin-exchange/state/exchange-state-unit.ts +0 -133
  190. package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +0 -118
  191. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +0 -118
  192. package/src/styles/features/exchange.css +0 -374
@@ -1,79 +1,55 @@
1
1
  /**
2
- * GENERATE-FROM-BUTTON P2/P4 the resource-generate flow modal.
2
+ * GENERATE-FROM-BUTTON P2/P4 FLOW-LIFECYCLE-CONVERGENCE P3
3
+ * GATHER-AT-THE-TOP P2 — the resource-generate flow modal.
3
4
  *
4
- * Drives the step machine: configure-gather review configure-generation.
5
- * `useResourceGather` is mocked so we control the gathered context and assert
6
- * how the step wires `gather()` (incl. the Phase-4 exclusion threading) and
7
- * `onGenerateSubmit`. `useSemiont` is mocked to feed the entity-type options.
5
+ * One composite stack, no step machine: the gather controls sit at the TOP,
6
+ * the gathered evidence appears below them once gather fires, and the
7
+ * generation params render beneath the evidence once context arrives
8
+ * [gather zone] [evidence] [act zone], the same grammar the resolve
9
+ * wizard expresses. Gather state arrives as PROPS (the page reads
10
+ * `gather.resourceContext$` and friends off the state unit) and the gather
11
+ * itself is an `onGather` callback — the modal mocks no hook and reaches for
12
+ * no provider (FLC A6). Entity-type options are owner-supplied, so a failed
13
+ * load cannot surface as an empty vocabulary.
8
14
  */
9
15
  import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
10
16
  import { render, screen, fireEvent } from '@testing-library/react';
11
17
  import '@testing-library/jest-dom';
12
- import { BehaviorSubject, of } from 'rxjs';
13
18
  import type { GatheredContext } from '@semiont/core';
19
+ import type { ResourceGatherOptions } from '@semiont/sdk';
14
20
  import type { GenerationConfig } from '../ConfigureGenerationStep';
21
+ import { ResourceGenerateModal } from '../ResourceGenerateModal';
15
22
 
16
23
  const RESOURCE_CONTEXT = {
17
- focus: { kind: 'resource', resource: { id: 'res-1', name: 'My Resource' }, summary: 'A short summary' },
24
+ focus: { kind: 'resource', resource: { '@id': 'res-1', name: 'My Resource' }, summary: 'A short summary' },
18
25
  graph: { nodes: [{ id: 'res-1', type: 'resource', label: 'My Resource' }], edges: [] },
19
26
  metadata: {},
20
27
  } as unknown as GatheredContext;
21
28
 
22
- // Mutable hook state + spies, shared with the hoisted vi.mock factory.
23
- const h = vi.hoisted(() => ({
24
- gather: vi.fn(),
25
- reset: vi.fn(),
26
- state: { context: null as unknown, loading: false, error: null as Error | null },
27
- }));
28
-
29
- vi.mock('../../../hooks/useResourceGather', () => ({
30
- useResourceGather: () => ({
31
- context: h.state.context,
32
- loading: h.state.loading,
33
- error: h.state.error,
34
- gather: h.gather,
35
- reset: h.reset,
36
- }),
37
- }));
38
-
39
- // The modal no longer fetches entity types; the owner supplies them, so a
40
- // failed load cannot surface as an empty vocabulary. The stable-observable
41
- // note below is kept because the client mock still backs other calls.
42
- // See .plans/PANEL-FAILURE-STATES.md
43
- // Stable observable instance — the modal calls `entityTypes()` inline in
44
- // render, so a fresh observable each call would re-subscribe every render and
45
- // loop. The real SDK returns a cached observable; mirror that here.
46
- const entityTypes$ = of<string[]>(['Person', 'Topic']);
47
- const mockClient = { browse: { entityTypes: () => entityTypes$ } };
48
- const activeSession$ = new BehaviorSubject<unknown>({ client: mockClient });
49
- vi.mock('../../../session/SemiontProvider', async () => {
50
- const actual = await vi.importActual<typeof import('../../../session/SemiontProvider')>('../../../session/SemiontProvider');
51
- return { ...actual, useSemiont: () => ({ activeSession$ }) };
52
- });
53
-
54
- import { ResourceGenerateModal } from '../ResourceGenerateModal';
55
-
56
29
  const T = {
57
- gatherTitle: 'Configure Gather',
58
- reviewTitle: 'Review Context',
59
- configureTitle: 'Configure Generation',
60
- next: 'Next',
61
- back: 'Back',
62
- cancel: 'Cancel',
30
+ title: 'Generate from this Resource',
63
31
  gatherIntro: 'Choose what to include.',
64
32
  includeContent: 'Include content',
65
33
  includeSummary: 'Include summary',
66
34
  gatherDepth: 'Depth',
67
35
  gatherMaxResources: 'Max resources',
68
36
  gatherButton: 'Gather',
69
- excludeLabel: 'Exclude from recall',
37
+ recallLabel: 'Included in recall — deselect to exclude',
70
38
  loadingContext: 'Gathering…',
71
39
  failedContext: 'Failed',
72
40
  sourceContextLabel: 'Resource',
73
41
  connectionsLabel: 'Connections',
74
42
  citedByLabel: 'Cited by',
43
+ graphPaneTitle: 'In the graph',
44
+ graphEmpty: 'No links yet.', resourceLinkLabel: 'Resource link',
45
+ corpusPaneTitle: 'In the corpus',
46
+ corpusEmpty: 'Nothing similar in the corpus.',
47
+ excludedReceipt: '{{types}} excluded from this recall',
48
+ machineRead: 'OCR',
49
+ score: 'Score',
75
50
  resourceTitle: 'New resource title',
76
51
  resourceTitlePlaceholder: 'Title…',
52
+ saveLocation: 'Save location',
77
53
  additionalInstructions: 'Additional Instructions',
78
54
  additionalInstructionsPlaceholder: 'Optional…',
79
55
  language: 'Language',
@@ -84,11 +60,18 @@ const T = {
84
60
  maxLength: 'Max Length',
85
61
  maxLengthHelp: 'Max help',
86
62
  maxLengthCeiling: 'Limited to {{maxOutputTokens}} tokens by {{model}}.',
63
+ outputFormat: 'Format',
64
+ formatExtensionMismatch: 'Save location must end in {{extension}} to match the selected format.',
87
65
  generate: 'Generate',
66
+ discardDraftPrompt: 'Discard this draft?',
67
+ discardDraft: 'Discard',
68
+ keepEditing: 'Keep editing',
69
+ editGather: 'Change gather settings',
88
70
  };
89
71
 
90
72
  let onClose: Mock<() => void>;
91
73
  let onGenerateSubmit: Mock<(resourceId: string, config: GenerationConfig) => void>;
74
+ let onGather: Mock<(options: ResourceGatherOptions) => void>;
92
75
 
93
76
  function renderModal(props: Partial<React.ComponentProps<typeof ResourceGenerateModal>> = {}) {
94
77
  return render(
@@ -100,6 +83,10 @@ function renderModal(props: Partial<React.ComponentProps<typeof ResourceGenerate
100
83
  locale="en"
101
84
  entityTypeOptions={['Person', 'Topic']}
102
85
  onGenerateSubmit={onGenerateSubmit}
86
+ gatherContext={null}
87
+ gatherLoading={false}
88
+ gatherError={null}
89
+ onGather={onGather}
103
90
  translations={T}
104
91
  {...props}
105
92
  />,
@@ -111,39 +98,81 @@ describe('ResourceGenerateModal', () => {
111
98
  vi.clearAllMocks();
112
99
  onClose = vi.fn<() => void>();
113
100
  onGenerateSubmit = vi.fn<(resourceId: string, config: GenerationConfig) => void>();
114
- h.state = { context: null, loading: false, error: null };
115
- activeSession$.next({ client: mockClient });
101
+ onGather = vi.fn<(options: ResourceGatherOptions) => void>();
116
102
  // jsdom doesn't implement scrollIntoView; GatherContextStep may call it.
117
103
  Element.prototype.scrollIntoView = vi.fn();
118
104
  });
119
105
 
120
106
  it('renders nothing when closed', () => {
121
107
  renderModal({ isOpen: false });
122
- expect(screen.queryByText('Configure Gather')).not.toBeInTheDocument();
108
+ expect(screen.queryByText(T.title)).not.toBeInTheDocument();
123
109
  });
124
110
 
125
- it('opens on the configure-gather step with the exclusion options', () => {
126
- renderModal();
127
- expect(screen.getByText('Configure Gather')).toBeInTheDocument(); // step title
128
- expect(screen.getByText('Choose what to include.')).toBeInTheDocument();
129
- expect(screen.getByLabelText('Include content')).toBeInTheDocument();
111
+ it('opens with the gather controls at the top and nothing below them yet', () => {
112
+ // Stale slots must not leak a previous run's evidence into a fresh open:
113
+ // even with a context PROP already present, nothing renders below the
114
+ // controls until THIS run's gather fires.
115
+ const { baseElement } = renderModal({ gatherContext: RESOURCE_CONTEXT });
116
+ expect(screen.getByText(T.title)).toBeInTheDocument(); // the one modal title
117
+ expect(screen.getByText(T.gatherIntro)).toBeInTheDocument();
118
+ expect(screen.getByLabelText(T.includeContent)).toBeInTheDocument();
130
119
  expect(screen.getByRole('button', { name: 'Person' })).toBeInTheDocument();
131
120
  expect(screen.getByRole('button', { name: 'Topic' })).toBeInTheDocument();
121
+ expect(baseElement.querySelector('.semiont-wizard__step-scroll')).not.toBeNull();
122
+ expect(screen.queryByText(T.graphPaneTitle)).not.toBeInTheDocument();
123
+ expect(screen.queryByLabelText(T.resourceTitle)).not.toBeInTheDocument();
132
124
  });
133
125
 
134
- it('submitting the gather step calls gather() and advances to review (exclusion omitted when none picked)', () => {
126
+ it('Gather emits onGather; the spent controls collapse to a receipt of what fired', () => {
127
+ // The modal no longer knows how to gather — it says WHAT to gather and the
128
+ // page wires the state unit (FLC D3). No resourceId in the payload: the
129
+ // owner already holds it.
130
+ const { baseElement } = renderModal({ gatherLoading: true });
131
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
132
+ expect(onGather).toHaveBeenCalledWith({ includeContent: true, includeSummary: true, depth: 2, maxResources: 10 });
133
+ expect(baseElement.querySelector('.semiont-gather__loading')).not.toBeNull();
134
+ // The form's job is done: it folds into a one-line receipt of what THIS
135
+ // gather asked for, and the evidence leads the pane. The form stays
136
+ // mounted (hidden) so its values survive for the expand path.
137
+ const receipt = screen.getByTitle(T.editGather);
138
+ expect(receipt.textContent).toContain(`${T.gatherDepth} 2`);
139
+ expect(receipt.textContent).toContain(`${T.gatherMaxResources} 10`);
140
+ expect(receipt.textContent).toContain(T.includeContent);
141
+ expect(screen.getByText(T.gatherIntro)).not.toBeVisible();
142
+ });
143
+
144
+ it('the receipt names the exclusions of THIS recall', () => {
135
145
  renderModal();
146
+ fireEvent.click(screen.getByRole('button', { name: 'Person' }));
136
147
  fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
137
- expect(h.gather).toHaveBeenCalledWith('res-1', { includeContent: true, includeSummary: true, depth: 2, maxResources: 10 });
138
- expect(screen.getByText('Review Context')).toBeInTheDocument(); // step title flipped
139
- expect(screen.getByRole('button', { name: /Next/ })).toBeDisabled(); // no context yet
148
+ expect(screen.getByTitle(T.editGather).textContent)
149
+ .toContain('Person excluded from this recall');
140
150
  });
141
151
 
142
- it('threads picked entity types into the gather call', () => {
152
+ it('the receipt expands back to the form — values preserved — and re-gather collapses again', () => {
153
+ renderModal({ gatherContext: RESOURCE_CONTEXT });
154
+ fireEvent.change(screen.getByLabelText(T.gatherDepth), { target: { value: '4' } });
155
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
156
+ expect(screen.getByTitle(T.editGather).textContent).toContain(`${T.gatherDepth} 4`);
157
+
158
+ fireEvent.click(screen.getByTitle(T.editGather));
159
+ const depthInput = screen.getByLabelText(T.gatherDepth);
160
+ expect(depthInput).toBeVisible();
161
+ expect(depthInput).toHaveValue(4); // not re-initialized — the form never unmounted
162
+ expect(screen.queryByTitle(T.editGather)).not.toBeInTheDocument();
163
+
164
+ fireEvent.change(depthInput, { target: { value: '3' } });
165
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
166
+ expect(onGather).toHaveBeenCalledTimes(2);
167
+ expect(onGather).toHaveBeenLastCalledWith(expect.objectContaining({ depth: 3 }));
168
+ expect(screen.getByTitle(T.editGather).textContent).toContain(`${T.gatherDepth} 3`);
169
+ });
170
+
171
+ it('threads crossed-off entity types into the gather options as the exclude list', () => {
143
172
  renderModal();
144
- fireEvent.click(screen.getByRole('button', { name: 'Person' })); // select to exclude
173
+ fireEvent.click(screen.getByRole('button', { name: 'Person' })); // cross off the recall
145
174
  fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
146
- expect(h.gather).toHaveBeenCalledWith('res-1', {
175
+ expect(onGather).toHaveBeenCalledWith({
147
176
  includeContent: true,
148
177
  includeSummary: true,
149
178
  depth: 2,
@@ -152,17 +181,34 @@ describe('ResourceGenerateModal', () => {
152
181
  });
153
182
  });
154
183
 
155
- it('walks gather review configure-generation emits onGenerateSubmit then closes', () => {
156
- h.state.context = RESOURCE_CONTEXT; // gather already resolved
184
+ it('every entity type starts INCLUDED deselecting is what excludes (recall chips)', () => {
185
+ // The SDK's wire is an exclude list, but the natural posture is
186
+ // "everything is in the recall until you say otherwise": chips render
187
+ // included by default and a click crosses one off.
157
188
  renderModal();
189
+ expect(screen.getByText(T.recallLabel)).toBeInTheDocument();
190
+ const person = screen.getByRole('button', { name: 'Person' });
191
+ const topic = screen.getByRole('button', { name: 'Topic' });
192
+ expect(person).toHaveAttribute('aria-pressed', 'true');
193
+ expect(person).toHaveAttribute('data-included', 'true');
194
+ expect(person.className).toContain('semiont-form__recall-chip');
195
+
196
+ fireEvent.click(person);
197
+ expect(person).toHaveAttribute('aria-pressed', 'false');
198
+ expect(person).toHaveAttribute('data-included', 'false');
199
+ expect(topic).toHaveAttribute('aria-pressed', 'true'); // the rest stay in
158
200
 
159
- fireEvent.click(screen.getByRole('button', { name: /Gather/ })); // review
160
- const next = screen.getByRole('button', { name: /Next/ });
161
- expect(next).toBeEnabled();
162
- fireEvent.click(next); // → configure-generation
201
+ fireEvent.click(person); // back in the toggle round-trips
202
+ expect(person).toHaveAttribute('aria-pressed', 'true');
203
+ });
204
+
205
+ it('params render beneath the evidence once context arrives; Generate emits and closes', () => {
206
+ renderModal({ gatherContext: RESOURCE_CONTEXT }); // gather resolves immediately
163
207
 
164
- fireEvent.change(screen.getByLabelText('New resource title'), { target: { value: 'Generated Doc' } });
165
- fireEvent.change(screen.getByLabelText('Save location'), { target: { value: 'generated/out.md' } });
208
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
209
+
210
+ fireEvent.change(screen.getByLabelText(T.resourceTitle), { target: { value: 'Generated Doc' } });
211
+ fireEvent.change(screen.getByLabelText(T.saveLocation), { target: { value: 'generated/out.md' } });
166
212
  fireEvent.click(screen.getByRole('button', { name: /Generate/ }));
167
213
 
168
214
  expect(onGenerateSubmit).toHaveBeenCalledTimes(1);
@@ -170,29 +216,66 @@ describe('ResourceGenerateModal', () => {
170
216
  'res-1',
171
217
  expect.objectContaining({
172
218
  title: 'Generated Doc',
173
- storagePath: 'file://generated/out.md',
219
+ // D8: the submitted payload names this what it IS — a full storage
220
+ // URI, built here by prefixing `file://`. It was called `storagePath`
221
+ // while holding a URI, which is why the page handlers had to rename
222
+ // it field-by-field on the way out.
223
+ storageUri: 'file://generated/out.md',
224
+ // D2: the default is sent, not inherited from the worker.
225
+ outputMediaType: 'text/markdown',
174
226
  context: RESOURCE_CONTEXT,
175
227
  }),
176
228
  );
177
229
  expect(onClose).toHaveBeenCalled();
178
230
  });
179
231
 
180
- it('Back from configure-generation returns to review', () => {
181
- h.state.context = RESOURCE_CONTEXT;
182
- renderModal();
183
- fireEvent.click(screen.getByRole('button', { name: /Gather/ })); // → review
184
- fireEvent.click(screen.getByRole('button', { name: /Next/ })); // → configure-generation
185
- expect(screen.getByText('Configure Generation')).toBeInTheDocument();
186
- fireEvent.click(screen.getByRole('button', { name: /Back/ })); // → review
187
- expect(screen.getByText('Review Context')).toBeInTheDocument();
232
+ it('a chosen output format survives the host to onGenerateSubmit', () => {
233
+ renderModal({ gatherContext: RESOURCE_CONTEXT });
234
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
235
+
236
+ fireEvent.change(screen.getByLabelText(T.resourceTitle), { target: { value: 'Generated Doc' } });
237
+ fireEvent.change(screen.getByLabelText(T.saveLocation), { target: { value: 'generated/out.pdf' } });
238
+ fireEvent.change(screen.getByLabelText(T.outputFormat), { target: { value: 'application/pdf' } });
239
+ fireEvent.click(screen.getByRole('button', { name: /Generate/ }));
240
+
241
+ expect(onGenerateSubmit).toHaveBeenCalledWith(
242
+ 'res-1',
243
+ expect.objectContaining({ outputMediaType: 'application/pdf', storageUri: 'file://generated/out.pdf' }),
244
+ );
188
245
  });
189
246
 
190
- it('Back from review returns to the configure-gather step', () => {
191
- renderModal();
192
- fireEvent.click(screen.getByRole('button', { name: /Gather/ })); // → review
193
- expect(screen.getByText('Review Context')).toBeInTheDocument();
194
- fireEvent.click(screen.getByRole('button', { name: /Back/ })); // → configure-gather
195
- expect(screen.getByText('Configure Gather')).toBeInTheDocument();
247
+ it('a failed gather shows its failure in place', () => {
248
+ // The props are the state unit's slots verbatim; the modal renders them
249
+ // without owning them (FLC A6).
250
+ renderModal({ gatherLoading: true });
251
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
252
+
253
+ render(
254
+ <ResourceGenerateModal
255
+ isOpen onClose={onClose} resourceId="res-1" defaultTitle="Default Title"
256
+ locale="en" entityTypeOptions={['Person', 'Topic']} onGenerateSubmit={onGenerateSubmit}
257
+ gatherContext={null} gatherLoading={false} gatherError={new Error('boom')}
258
+ onGather={onGather} translations={T}
259
+ />,
260
+ );
261
+ fireEvent.click(screen.getAllByRole('button', { name: /Gather/ }).at(-1)!);
262
+ expect(screen.getByText(T.failedContext)).toBeInTheDocument();
263
+ });
264
+
265
+ it('re-Gather refreshes the evidence in place — the stack stays', () => {
266
+ renderModal({ gatherContext: RESOURCE_CONTEXT });
267
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
268
+ fireEvent.click(screen.getByTitle(T.editGather)); // expand the receipt
269
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ })); // tweak-and-regather
270
+ expect(onGather).toHaveBeenCalledTimes(2);
271
+ expect(screen.getByText(T.graphPaneTitle)).toBeInTheDocument();
272
+ expect(screen.getByLabelText(T.resourceTitle)).toBeInTheDocument();
273
+ });
274
+
275
+ it('no Back anywhere — a single stack has nothing to go back to (D6)', () => {
276
+ renderModal({ gatherContext: RESOURCE_CONTEXT });
277
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
278
+ expect(screen.queryAllByRole('button', { name: /Back/ })).toHaveLength(0);
196
279
  });
197
280
 
198
281
  it('the header close button calls onClose', () => {
@@ -200,4 +283,135 @@ describe('ResourceGenerateModal', () => {
200
283
  fireEvent.click(screen.getByLabelText('Close'));
201
284
  expect(onClose).toHaveBeenCalledTimes(1);
202
285
  });
286
+
287
+ it('opening seeds the proposed title from defaultTitle, even one that arrived after mount (GFR A4)', () => {
288
+ // The page passes the source resource's name, which loads asynchronously —
289
+ // a draft seeded only in the useState initializer holds whatever was there
290
+ // at FIRST render, which for the real page was ''. Opening is the moment
291
+ // that matters: the draft re-seeds then.
292
+ const { rerender } = render(
293
+ <ResourceGenerateModal
294
+ isOpen={false} onClose={onClose} resourceId="res-1" defaultTitle=""
295
+ locale="en" entityTypeOptions={[]} onGenerateSubmit={onGenerateSubmit}
296
+ gatherContext={RESOURCE_CONTEXT} gatherLoading={false} gatherError={null}
297
+ onGather={onGather} translations={T}
298
+ />,
299
+ );
300
+ rerender(
301
+ <ResourceGenerateModal
302
+ isOpen onClose={onClose} resourceId="res-1" defaultTitle="PB"
303
+ locale="en" entityTypeOptions={[]} onGenerateSubmit={onGenerateSubmit}
304
+ gatherContext={RESOURCE_CONTEXT} gatherLoading={false} gatherError={null}
305
+ onGather={onGather} translations={T}
306
+ />,
307
+ );
308
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
309
+ expect(screen.getByLabelText(T.resourceTitle)).toHaveValue('PB');
310
+ });
311
+
312
+ it('evidence stays in view above the params — ONE scroll pane, nothing scrolls alone (A3)', () => {
313
+ const { baseElement } = renderModal({ gatherContext: RESOURCE_CONTEXT });
314
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
315
+
316
+ const scroll = baseElement.querySelector('.semiont-wizard__step-scroll');
317
+ expect(scroll).not.toBeNull();
318
+ expect(scroll!.querySelector('.semiont-gather__outer')).not.toBeNull(); // the evidence
319
+ expect(screen.getByText(T.graphPaneTitle)).toBeInTheDocument(); // graph pane up
320
+ expect(screen.getByLabelText(T.resourceTitle)).toBeInTheDocument(); // the form, below
321
+ for (const form of Array.from(scroll!.querySelectorAll('form.semiont-form'))) {
322
+ expect(form.className).not.toContain('semiont-form--scrollable');
323
+ }
324
+ });
325
+
326
+ it('the panel carries the widened class (GEP P2, D2)', () => {
327
+ const { baseElement } = renderModal();
328
+ const panel = baseElement.querySelector('.semiont-search-modal__panel--gather');
329
+ expect(panel).not.toBeNull();
330
+ expect(panel!.className).toContain('semiont-search-modal__panel--wide');
331
+ });
332
+
333
+ it('every footer is the wizard footer — no dismissal, no flex (GFR A5)', () => {
334
+ // The modal renders via a HeadlessUI portal, so query the whole document.
335
+ const { baseElement } = renderModal({ gatherContext: RESOURCE_CONTEXT });
336
+ const footerPins = () => {
337
+ expect(baseElement.querySelector('.semiont-modal__actions--wizard')).not.toBeNull();
338
+ expect(baseElement.querySelectorAll('.semiont-button--flex')).toHaveLength(0);
339
+ const footerButtons = Array.from(
340
+ baseElement.querySelectorAll('.semiont-modal__actions button'),
341
+ ).map((b) => b.textContent ?? '');
342
+ expect(footerButtons.filter((l) => /cancel|✕/i.test(l))).toEqual([]);
343
+ };
344
+
345
+ footerPins(); // controls alone
346
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
347
+ footerPins(); // the full stack: Gather footer + Generate footer
348
+ });
349
+ });
350
+
351
+ // GATHER-AT-THE-TOP P1: dismissal guards typed work. D5 — typed text only
352
+ // (title beyond the seeded default, save location, instructions); checkbox,
353
+ // depth, and exclusion state is cheap to redo and never nags.
354
+ describe('ResourceGenerateModal — dismissal guards typed work (GATHER-AT-THE-TOP P1)', () => {
355
+ beforeEach(() => {
356
+ onClose = vi.fn();
357
+ onGenerateSubmit = vi.fn();
358
+ onGather = vi.fn();
359
+ });
360
+
361
+ it('a typed save location guards dismissal; Keep editing stays, Discard closes', () => {
362
+ renderModal({ gatherContext: RESOURCE_CONTEXT });
363
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ })); // reveal the stack
364
+ fireEvent.change(screen.getByLabelText(T.saveLocation), { target: { value: 'generated/out.md' } });
365
+
366
+ fireEvent.click(screen.getByLabelText('Close'));
367
+ expect(onClose).not.toHaveBeenCalled();
368
+ expect(screen.getByText(T.discardDraftPrompt)).toBeInTheDocument();
369
+
370
+ fireEvent.click(screen.getByRole('button', { name: T.keepEditing }));
371
+ expect(screen.queryByText(T.discardDraftPrompt)).not.toBeInTheDocument();
372
+ expect(screen.getByLabelText(T.saveLocation)).toHaveValue('generated/out.md');
373
+ expect(onClose).not.toHaveBeenCalled();
374
+
375
+ fireEvent.click(screen.getByLabelText('Close'));
376
+ fireEvent.click(screen.getByRole('button', { name: T.discardDraft }));
377
+ expect(onClose).toHaveBeenCalledTimes(1);
378
+ });
379
+
380
+ it('a clean draft dismisses without any prompt', () => {
381
+ renderModal({ gatherContext: RESOURCE_CONTEXT });
382
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
383
+
384
+ fireEvent.click(screen.getByLabelText('Close'));
385
+ expect(screen.queryByText(T.discardDraftPrompt)).not.toBeInTheDocument();
386
+ expect(onClose).toHaveBeenCalledTimes(1);
387
+ });
388
+
389
+ it('a defaultTitle arriving mid-flow does not fake a dirty draft', () => {
390
+ // The source resource's name loads asynchronously: the baseline moves
391
+ // while the modal is open, the user has typed nothing. Dirtiness compares
392
+ // against what was SEEDED, not the live prop.
393
+ const { rerender } = renderModal({ defaultTitle: '', gatherContext: RESOURCE_CONTEXT });
394
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
395
+ rerender(
396
+ <ResourceGenerateModal
397
+ isOpen onClose={onClose} resourceId="res-1" defaultTitle="Loaded Name"
398
+ locale="en" entityTypeOptions={['Person', 'Topic']} onGenerateSubmit={onGenerateSubmit}
399
+ gatherContext={RESOURCE_CONTEXT} gatherLoading={false} gatherError={null}
400
+ onGather={onGather} translations={T}
401
+ />,
402
+ );
403
+
404
+ fireEvent.click(screen.getByLabelText('Close'));
405
+ expect(screen.queryByText(T.discardDraftPrompt)).not.toBeInTheDocument();
406
+ expect(onClose).toHaveBeenCalledTimes(1);
407
+ });
408
+
409
+ it('non-text state alone never nags — exclusions and depth are cheap to redo (D5)', () => {
410
+ renderModal();
411
+ fireEvent.click(screen.getByRole('button', { name: 'Person' })); // cross off the recall
412
+
413
+ fireEvent.click(screen.getByLabelText('Close'));
414
+ expect(screen.queryByText(T.discardDraftPrompt)).not.toBeInTheDocument();
415
+ expect(onClose).toHaveBeenCalledTimes(1);
416
+ });
203
417
  });
@@ -0,0 +1,61 @@
1
+ /**
2
+ * GATHER-EVIDENCE-PANES D10 (amended 2026-08-19) — `SearchResultsStep` is pure
3
+ * RESULTS: ranked rows with scores and Link buttons, nothing else.
4
+ *
5
+ * The evidence lives with the HOST: the wizard stacks the full display-only
6
+ * `GatherContextStep` (quotation, graph pane, corpus pane, collapsed strategy
7
+ * band) above this step, exactly as on the configure steps. The original D10
8
+ * concern — corpus matches beside real results reading as a second, staler
9
+ * ranking — dissolved with the stacking: evidence sits above the strategy
10
+ * band, results below it, no competing side-by-side columns. This step
11
+ * therefore renders NO context of its own; a gather pane appearing here means
12
+ * someone re-embedded the minimal context redux this amendment deleted.
13
+ */
14
+ import { describe, it, expect, vi } from 'vitest';
15
+ import { render, screen } from '@testing-library/react';
16
+ import '@testing-library/jest-dom';
17
+ import { SearchResultsStep } from '../SearchResultsStep';
18
+
19
+ const T = { noResults: 'None', score: 'Score', link: 'Link', back: 'Back' };
20
+
21
+ const RESULTS = [
22
+ { '@id': 'r1', name: 'Martin Pauls', score: 57.46, matchReason: 'connected; semantic match' },
23
+ { '@id': 'r2', name: 'Arthur Fisher', score: 25.46, description: 'a citing resource' },
24
+ ] as never[];
25
+
26
+ describe('SearchResultsStep — pure results (D10 amended)', () => {
27
+ it('renders ranked rows: name, score, reason, and a Link button each', () => {
28
+ render(
29
+ <SearchResultsStep results={RESULTS} onBack={vi.fn()} onLink={vi.fn()} translations={T} />,
30
+ );
31
+ expect(screen.getByText('Martin Pauls')).toBeInTheDocument();
32
+ expect(screen.getByText(`${T.score}: 57.46`)).toBeInTheDocument();
33
+ expect(screen.getByText('connected; semantic match')).toBeInTheDocument();
34
+ expect(screen.getByText('a citing resource')).toBeInTheDocument();
35
+ expect(screen.getAllByRole('button', { name: `🔗 ${T.link}` })).toHaveLength(2);
36
+ });
37
+
38
+ it('links the row that was clicked, by @id', () => {
39
+ const onLink = vi.fn();
40
+ render(
41
+ <SearchResultsStep results={RESULTS} onBack={vi.fn()} onLink={onLink} translations={T} />,
42
+ );
43
+ screen.getAllByRole('button', { name: `🔗 ${T.link}` })[1]!.click();
44
+ expect(onLink).toHaveBeenCalledWith('r2');
45
+ });
46
+
47
+ it('shows the empty state when nothing matched', () => {
48
+ render(
49
+ <SearchResultsStep results={[]} onBack={vi.fn()} onLink={vi.fn()} translations={T} />,
50
+ );
51
+ expect(screen.getByText(T.noResults)).toBeInTheDocument();
52
+ });
53
+
54
+ it('renders no context of its own — the host owns the evidence', () => {
55
+ const { container } = render(
56
+ <SearchResultsStep results={RESULTS} onBack={vi.fn()} onLink={vi.fn()} translations={T} />,
57
+ );
58
+ expect(container.querySelector('.semiont-gather-pane')).toBeNull();
59
+ expect(container.querySelector('.semiont-search-results__two-pane')).toBeNull();
60
+ });
61
+ });