@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,21 +1,17 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useCallback } from 'react';
4
- import type { CollaboratorEntry } from '@semiont/core';
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+ import type { CollaboratorEntry, GatheredContext } from '@semiont/core';
5
+ import type { ResourceGatherOptions } from '@semiont/sdk';
5
6
  import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
6
- import { useResourceGather } from '../../hooks/useResourceGather';
7
7
  import { ConfigureGatherStep, type ResourceGatherConfig } from './ConfigureGatherStep';
8
+ import { DiscardPrompt } from './DiscardPrompt';
8
9
  import { GatherContextStep } from './GatherContextStep';
9
- import { ConfigureGenerationStep, type GenerationConfig, type GenerationDraft } from './ConfigureGenerationStep';
10
+ import { ConfigureGenerationStep, freshGenerationDraft, type GenerationConfig, type GenerationDraft } from './ConfigureGenerationStep';
10
11
 
11
12
  export interface ResourceGenerateModalTranslations {
12
- // Step titles + nav
13
- gatherTitle: string;
14
- reviewTitle: string;
15
- configureTitle: string;
16
- next: string;
17
- back: string;
18
- cancel: string;
13
+ /** The ONE modal title — names the flow, not a step. */
14
+ title: string;
19
15
  // ConfigureGatherStep
20
16
  gatherIntro: string;
21
17
  includeContent: string;
@@ -23,16 +19,28 @@ export interface ResourceGenerateModalTranslations {
23
19
  gatherDepth: string;
24
20
  gatherMaxResources: string;
25
21
  gatherButton: string;
26
- excludeLabel: string;
22
+ /** Labels the recall chips: every type included until deselected. */
23
+ recallLabel: string;
24
+ /** Affordance on the post-gather receipt row (title/tooltip). */
25
+ editGather: string;
27
26
  // GatherContextStep display
28
27
  loadingContext: string;
29
28
  failedContext: string;
30
29
  sourceContextLabel: string;
31
30
  connectionsLabel: string;
32
31
  citedByLabel: string;
32
+ graphPaneTitle: string;
33
+ graphEmpty: string;
34
+ resourceLinkLabel: string;
35
+ corpusPaneTitle: string;
36
+ corpusEmpty: string;
37
+ excludedReceipt: string;
38
+ machineRead: string;
39
+ score: string;
33
40
  // ConfigureGenerationStep
34
41
  resourceTitle: string;
35
42
  resourceTitlePlaceholder: string;
43
+ saveLocation: string;
36
44
  additionalInstructions: string;
37
45
  additionalInstructionsPlaceholder: string;
38
46
  language: string;
@@ -43,7 +51,13 @@ export interface ResourceGenerateModalTranslations {
43
51
  maxLength: string;
44
52
  maxLengthHelp: string;
45
53
  maxLengthCeiling: string;
54
+ outputFormat: string;
55
+ formatExtensionMismatch: string;
46
56
  generate: string;
57
+ // DiscardPrompt (GATHER-AT-THE-TOP P1)
58
+ discardDraftPrompt: string;
59
+ discardDraft: string;
60
+ keepEditing: string;
47
61
  }
48
62
 
49
63
  export interface ResourceGenerateModalProps {
@@ -57,8 +71,9 @@ export interface ResourceGenerateModalProps {
57
71
  onClose: () => void;
58
72
  resourceId: string;
59
73
  defaultTitle: string;
74
+ /** Folder of the source resource, so the artifact lands beside it (D11). */
75
+ defaultFolder?: string;
60
76
  locale: string;
61
- gatherDefaults?: Partial<ResourceGatherConfig>;
62
77
  /**
63
78
  * Entity types offered in the exclusion picker. Owner-supplied so the
64
79
  * modal cannot present a failed load as an empty vocabulary.
@@ -70,70 +85,161 @@ export interface ResourceGenerateModalProps {
70
85
  * annotation wizard delegates generation to its parent.
71
86
  */
72
87
  onGenerateSubmit: (resourceId: string, config: GenerationConfig) => void;
88
+ /**
89
+ * Resource-gather state, verbatim from the SDK's gather unit slots
90
+ * (`gather.resourceContext$` / `resourceLoading$` / `resourceError$`) — the
91
+ * page reads the observables and threads them here, the same shape the
92
+ * reference wizard has always had (FLOW-LIFECYCLE-CONVERGENCE D3/A6). The
93
+ * modal renders this state; it never owns it.
94
+ */
95
+ gatherContext: GatheredContext | null;
96
+ gatherLoading: boolean;
97
+ gatherError: Error | null;
98
+ /**
99
+ * Run the gather — the page wires `gather.gatherResource(resourceId, …)`.
100
+ * No resourceId in the payload: the owner already holds it. `gatherResource`
101
+ * clears its slots at start, so no reset threading is needed.
102
+ */
103
+ onGather: (options: ResourceGatherOptions) => void;
73
104
  translations: ResourceGenerateModalTranslations;
74
105
  }
75
106
 
76
- type Step = 'configure-gather' | 'review' | 'configure-generation';
77
-
78
107
  /**
79
- * Resource-generate flow (GENERATE-FROM-BUTTON): configure gather options →
80
- * `gather.resource` review the gathered `GatheredContext` configure
81
- * generation → emit. Reuses the kind-aware `GatherContextStep` for the review.
108
+ * Resource-generate flow (GENERATE-FROM-BUTTON, folded by GATHER-AT-THE-TOP):
109
+ * one composite stack in a single scroll pane — the gather controls at the
110
+ * top, the gathered `GatheredContext` below them once gather fires, and the
111
+ * generation params beneath the evidence once context arrives. Re-gathering
112
+ * is scroll-up → tweak → Gather; `gatherResource` clears its slots at start,
113
+ * so the evidence refreshes in place. [gather zone] → [evidence] → [act
114
+ * zone]: the same grammar the resolve wizard expresses, whose gather zone
115
+ * happens to be automatic and whose act zone has a strategy choice.
82
116
  */
83
117
  export function ResourceGenerateModal({
84
118
  isOpen,
85
119
  onClose,
86
120
  resourceId,
87
121
  defaultTitle,
122
+ defaultFolder,
88
123
  locale,
89
- gatherDefaults,
90
124
  entityTypeOptions = [],
91
125
  onGenerateSubmit,
126
+ gatherContext,
127
+ gatherLoading,
128
+ gatherError,
129
+ onGather,
92
130
  translations: t,
93
131
  generationAgent,
94
132
  }: ResourceGenerateModalProps) {
95
133
  // Same draft ownership as the wizard (WIZARD-NAVIGATION D3): the step is
96
134
  // controlled, so stepping back through this modal keeps what was typed.
97
- const [generationDraft, setGenerationDraft] = useState<GenerationDraft>({
98
- title: defaultTitle, storagePath: '', prompt: '', language: locale,
99
- temperature: 0.7, maxTokensText: '500',
100
- });
135
+ const freshDraft = (): GenerationDraft => freshGenerationDraft(defaultTitle, locale);
136
+ const [generationDraft, setGenerationDraft] = useState<GenerationDraft>(freshDraft);
101
137
 
102
- const [step, setStep] = useState<Step>('configure-gather');
103
- const { context, loading, error, gather, reset } = useResourceGather();
138
+ // "Has THIS run gathered yet?" — the page's gather slots survive across
139
+ // opens, so a context prop can be stale at open time. Nothing renders below
140
+ // the controls until the user fires a gather in this run.
141
+ const [gatherFired, setGatherFired] = useState(false);
142
+ // The fired snapshot feeds the receipt: what THIS gather actually asked
143
+ // for, exclusions included — not the form's live state, which the user can
144
+ // edit without re-firing.
145
+ const [lastGather, setLastGather] = useState<(ResourceGatherConfig & { excludeEntityTypes: string[] }) | null>(null);
146
+ const [editingGather, setEditingGather] = useState(false);
147
+ const [showDiscardPrompt, setShowDiscardPrompt] = useState(false);
104
148
  // Supplied by the owner, which already tracks the list with its failure
105
149
  // state. Fetching it here could only model (value | not-yet), so a failed
106
150
  // load would render an empty exclusion picker as though the KB had no
107
151
  // entity types. See .plans/PANEL-FAILURE-STATES.md
108
152
  const [excludeEntityTypes, setExcludeEntityTypes] = useState<string[]>([]);
109
153
 
110
- // Reset to the first step whenever the modal (re)opens.
154
+ // Reset to the first step ON OPENING — and re-seed the draft, because
155
+ // `defaultTitle` is the source resource's name and loads asynchronously: the
156
+ // useState initializer ran at mount, which for the real page was before the
157
+ // name existed (GFR A4). Guarded to the false→true transition so a name
158
+ // arriving mid-flow cannot clobber what the user has typed.
159
+ const wasOpen = useRef(false);
160
+ // The dirty baseline is what was SEEDED, not the live prop: defaultTitle
161
+ // can move while the modal is open (the source resource's name loads
162
+ // asynchronously), and an untouched draft must not read as dirty because
163
+ // the baseline moved under it.
164
+ const seededTitle = useRef(defaultTitle);
111
165
  useEffect(() => {
112
- if (isOpen) {
113
- setStep('configure-gather');
166
+ if (isOpen && !wasOpen.current) {
167
+ setGatherFired(false);
168
+ setLastGather(null);
169
+ setEditingGather(false);
114
170
  setExcludeEntityTypes([]);
115
- reset();
171
+ setGenerationDraft(freshDraft());
172
+ setShowDiscardPrompt(false);
173
+ seededTitle.current = defaultTitle;
116
174
  }
117
- }, [isOpen, reset]);
175
+ wasOpen.current = isOpen;
176
+ // freshDraft reads defaultTitle/locale at call time; the effect keys on the
177
+ // OPENING, not on their drift while open.
178
+ // eslint-disable-next-line react-hooks/exhaustive-deps
179
+ }, [isOpen]);
118
180
 
119
181
  const handleGather = useCallback((config: ResourceGatherConfig) => {
120
- setStep('review');
121
- void gather(resourceId, {
182
+ setGatherFired(true);
183
+ setLastGather({ ...config, excludeEntityTypes });
184
+ setEditingGather(false);
185
+ onGather({
122
186
  ...config,
123
187
  ...(excludeEntityTypes.length ? { excludeEntityTypes } : {}),
124
188
  });
125
- }, [gather, resourceId, excludeEntityTypes]);
189
+ }, [onGather, excludeEntityTypes]);
126
190
 
127
191
  const handleGenerate = useCallback((config: GenerationConfig) => {
128
192
  onGenerateSubmit(resourceId, config);
129
193
  onClose();
130
194
  }, [onGenerateSubmit, resourceId, onClose]);
131
195
 
132
- const stepTitle = step === 'configure-gather' ? t.gatherTitle : step === 'review' ? t.reviewTitle : t.configureTitle;
196
+ // D4/D5 (GATHER-AT-THE-TOP P1): typed text must not die with a dismissed
197
+ // modal. Toggles, depth, and exclusion picks are cheap to redo and never
198
+ // nag. Generate's completion path above bypasses this — the guard protects
199
+ // dismissal, not completion.
200
+ const draftDirty =
201
+ generationDraft.title !== seededTitle.current ||
202
+ generationDraft.storagePath.trim() !== '' ||
203
+ generationDraft.prompt.trim() !== '';
204
+ const handleDismiss = useCallback(() => {
205
+ if (draftDirty) {
206
+ setShowDiscardPrompt(true);
207
+ return;
208
+ }
209
+ onClose();
210
+ }, [draftDirty, onClose]);
211
+
212
+ // The whole stack is ONE scroll pane; when the params zone appears (context
213
+ // arrival — including each re-gather), enter at the BOTTOM so the
214
+ // parameters show in full, the evidence and controls tucked up under the
215
+ // modal top. jsdom has no layout (scrollHeight 0) — no-op in tests.
216
+ const stepScrollRef = useRef<HTMLDivElement>(null);
217
+ useEffect(() => {
218
+ const el = stepScrollRef.current;
219
+ if (el && gatherContext) el.scrollTop = el.scrollHeight;
220
+ }, [gatherContext]);
221
+
222
+ // The evidence zone: what the generation will be grounded in stays in view
223
+ // above the form.
224
+ const displayTranslations = {
225
+ loadingContext: t.loadingContext,
226
+ failedContext: t.failedContext,
227
+ sourceContextLabel: t.sourceContextLabel,
228
+ connectionsLabel: t.connectionsLabel,
229
+ citedByLabel: t.citedByLabel,
230
+ graphPaneTitle: t.graphPaneTitle,
231
+ graphEmpty: t.graphEmpty,
232
+ resourceLinkLabel: t.resourceLinkLabel,
233
+ corpusPaneTitle: t.corpusPaneTitle,
234
+ corpusEmpty: t.corpusEmpty,
235
+ excludedReceipt: t.excludedReceipt,
236
+ machineRead: t.machineRead,
237
+ score: t.score,
238
+ };
133
239
 
134
240
  return (
135
241
  <Transition appear show={isOpen}>
136
- <Dialog as="div" className="semiont-search-modal" onClose={onClose}>
242
+ <Dialog as="div" className="semiont-search-modal" onClose={handleDismiss}>
137
243
  <TransitionChild
138
244
  enter="ease-out duration-200"
139
245
  enterFrom="opacity-0"
@@ -155,21 +261,53 @@ export function ResourceGenerateModal({
155
261
  leaveFrom="opacity-100 scale-100"
156
262
  leaveTo="opacity-0 scale-95"
157
263
  >
158
- <DialogPanel className="semiont-search-modal__panel semiont-search-modal__panel--with-border semiont-search-modal__panel--gather">
264
+ <DialogPanel className="semiont-search-modal__panel semiont-search-modal__panel--with-border semiont-search-modal__panel--gather semiont-search-modal__panel--wide">
159
265
  <div className="semiont-search-modal__header">
160
- <DialogTitle className="semiont-search-modal__title">{stepTitle}</DialogTitle>
161
- <button onClick={onClose} className="semiont-search-modal__close-button" aria-label="Close">
266
+ <DialogTitle className="semiont-search-modal__title">{t.title}</DialogTitle>
267
+ <button onClick={handleDismiss} className="semiont-search-modal__close-button" aria-label="Close">
162
268
 
163
269
  </button>
164
270
  </div>
165
271
 
166
- {step === 'configure-gather' && (
272
+ {showDiscardPrompt && (
273
+ <DiscardPrompt
274
+ prompt={t.discardDraftPrompt}
275
+ discardLabel={t.discardDraft}
276
+ keepLabel={t.keepEditing}
277
+ onDiscard={() => { setShowDiscardPrompt(false); onClose(); }}
278
+ onKeepEditing={() => setShowDiscardPrompt(false)}
279
+ />
280
+ )}
281
+
282
+ <div className="semiont-wizard__step-scroll" ref={stepScrollRef}>
283
+ {/* Once fired, the spent controls fold into a receipt of
284
+ what THIS gather asked for — the evidence leads the
285
+ pane, the parameters stay visible and revisable. The
286
+ form below is HIDDEN, not unmounted, so its values
287
+ survive for the expand path. */}
288
+ {gatherFired && !editingGather && lastGather && (
289
+ <button
290
+ type="button"
291
+ className="semiont-gather-receipt"
292
+ title={t.editGather}
293
+ onClick={() => setEditingGather(true)}
294
+ >
295
+ {[
296
+ `${t.gatherDepth} ${lastGather.depth}`,
297
+ `${t.gatherMaxResources} ${lastGather.maxResources}`,
298
+ ...(lastGather.includeContent ? [t.includeContent] : []),
299
+ ...(lastGather.includeSummary ? [t.includeSummary] : []),
300
+ ...(lastGather.excludeEntityTypes.length
301
+ ? [t.excludedReceipt.replace('{{types}}', lastGather.excludeEntityTypes.join(', '))]
302
+ : []),
303
+ ].join(' · ')}
304
+ <span aria-hidden="true"> ✎</span>
305
+ </button>
306
+ )}
307
+ <div hidden={gatherFired && !editingGather}>
167
308
  <ConfigureGatherStep
168
- defaults={gatherDefaults}
169
309
  onGather={handleGather}
170
- onCancel={onClose}
171
310
  translations={{
172
- cancel: t.cancel,
173
311
  intro: t.gatherIntro,
174
312
  includeContent: t.includeContent,
175
313
  includeSummary: t.includeSummary,
@@ -180,17 +318,21 @@ export function ResourceGenerateModal({
180
318
  >
181
319
  {entityTypeOptions.length > 0 && (
182
320
  <div className="semiont-form__field semiont-form__entity-types">
183
- <label className="semiont-form__label">{t.excludeLabel}</label>
184
- <div className="semiont-form__entity-type-buttons">
321
+ <label className="semiont-form__label">{t.recallLabel}</label>
322
+ {/* Recall chips — the exclude wire, inverted for the
323
+ UI: every type is IN the recall until crossed off,
324
+ so the state is still the exclude list and a chip
325
+ is included when absent from it. */}
326
+ <div className="semiont-form__recall-chips">
185
327
  {entityTypeOptions.map((et) => {
186
- const isSelected = excludeEntityTypes.includes(et);
328
+ const isIncluded = !excludeEntityTypes.includes(et);
187
329
  return (
188
330
  <button
189
331
  key={et}
190
332
  type="button"
191
- className="semiont-form__entity-type-button"
192
- data-selected={isSelected}
193
- aria-pressed={isSelected}
333
+ className="semiont-form__recall-chip"
334
+ data-included={isIncluded}
335
+ aria-pressed={isIncluded}
194
336
  onClick={() => setExcludeEntityTypes(prev => prev.includes(et) ? prev.filter(x => x !== et) : [...prev, et])}
195
337
  >
196
338
  {et}
@@ -201,75 +343,46 @@ export function ResourceGenerateModal({
201
343
  </div>
202
344
  )}
203
345
  </ConfigureGatherStep>
204
- )}
346
+ </div>
205
347
 
206
- {step === 'review' && (
207
- <>
348
+ {gatherFired && (
208
349
  <GatherContextStep
209
- context={context}
210
- contextLoading={loading}
211
- contextError={error}
350
+ context={gatherContext}
351
+ contextLoading={gatherLoading}
352
+ contextError={gatherError}
353
+ translations={displayTranslations}
354
+ />
355
+ )}
356
+
357
+ {gatherFired && gatherContext && (
358
+ <ConfigureGenerationStep
359
+ {...(generationAgent ? { generationAgent } : {})}
360
+ context={gatherContext}
361
+ config={generationDraft}
362
+ onConfigChange={setGenerationDraft}
363
+ defaultFolder={defaultFolder}
364
+ onGenerate={handleGenerate}
212
365
  translations={{
213
- title: '',
214
- loadingContext: t.loadingContext,
215
- failedContext: t.failedContext,
216
- search: '',
217
- generate: '',
218
- compose: '',
219
- resolutionStrategyLabel: '',
220
- sourceContextLabel: t.sourceContextLabel,
221
- connectionsLabel: t.connectionsLabel,
222
- citedByLabel: t.citedByLabel,
223
- userHintLabel: '',
224
- userHintPlaceholder: '',
366
+ resourceTitle: t.resourceTitle,
367
+ resourceTitlePlaceholder: t.resourceTitlePlaceholder,
368
+ saveLocation: t.saveLocation,
369
+ additionalInstructions: t.additionalInstructions,
370
+ additionalInstructionsPlaceholder: t.additionalInstructionsPlaceholder,
371
+ language: t.language,
372
+ languageHelp: t.languageHelp,
373
+ creativity: t.creativity,
374
+ creativityFocused: t.creativityFocused,
375
+ creativityCreative: t.creativityCreative,
376
+ maxLength: t.maxLength,
377
+ maxLengthHelp: t.maxLengthHelp,
378
+ maxLengthCeiling: t.maxLengthCeiling,
379
+ outputFormat: t.outputFormat,
380
+ formatExtensionMismatch: t.formatExtensionMismatch,
381
+ generate: t.generate,
225
382
  }}
226
383
  />
227
- <div className="semiont-modal__actions" style={{ paddingTop: '0.5rem' }}>
228
- <button
229
- type="button"
230
- onClick={() => setStep('configure-gather')}
231
- className="semiont-button--secondary semiont-button--flex"
232
- >
233
- ◀ {t.back}
234
- </button>
235
- <button
236
- type="button"
237
- onClick={() => setStep('configure-generation')}
238
- disabled={!context}
239
- className="semiont-button--primary semiont-button--flex"
240
- >
241
- {t.next} ▶
242
- </button>
243
- </div>
244
- </>
245
- )}
246
-
247
- {step === 'configure-generation' && context && (
248
- <ConfigureGenerationStep
249
- {...(generationAgent ? { generationAgent } : {})}
250
- context={context}
251
- config={generationDraft}
252
- onConfigChange={setGenerationDraft}
253
- onBack={() => setStep('review')}
254
- onGenerate={handleGenerate}
255
- translations={{
256
- resourceTitle: t.resourceTitle,
257
- resourceTitlePlaceholder: t.resourceTitlePlaceholder,
258
- additionalInstructions: t.additionalInstructions,
259
- additionalInstructionsPlaceholder: t.additionalInstructionsPlaceholder,
260
- language: t.language,
261
- languageHelp: t.languageHelp,
262
- creativity: t.creativity,
263
- creativityFocused: t.creativityFocused,
264
- creativityCreative: t.creativityCreative,
265
- maxLength: t.maxLength,
266
- maxLengthHelp: t.maxLengthHelp,
267
- maxLengthCeiling: t.maxLengthCeiling,
268
- back: t.back,
269
- generate: t.generate,
270
- }}
271
- />
272
- )}
384
+ )}
385
+ </div>
273
386
  </DialogPanel>
274
387
  </TransitionChild>
275
388
  </div>