@semiont/react-ui 0.5.8 → 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 (165) 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 +28 -1449
  38. package/dist/integrations/styled-components-theme.js.map +1 -1
  39. package/dist/{it-AS5GM232.js → it-UYH642IJ.js} +45 -4
  40. package/dist/it-UYH642IJ.js.map +1 -0
  41. package/dist/{ja-GZ4HLUOF.js → ja-JTVWAC4L.js} +45 -4
  42. package/dist/ja-JTVWAC4L.js.map +1 -0
  43. package/dist/{ko-A4XUXFGJ.js → ko-IHXFMOIZ.js} +45 -4
  44. package/dist/ko-IHXFMOIZ.js.map +1 -0
  45. package/dist/{ms-YBAO3S6K.js → ms-JH25VFLO.js} +45 -4
  46. package/dist/ms-JH25VFLO.js.map +1 -0
  47. package/dist/{nl-3TJGIIIU.js → nl-SURXW7IQ.js} +45 -4
  48. package/dist/nl-SURXW7IQ.js.map +1 -0
  49. package/dist/{no-4AXIQPRQ.js → no-HXIKQFDZ.js} +45 -4
  50. package/dist/no-HXIKQFDZ.js.map +1 -0
  51. package/dist/{pl-5GP6GKXO.js → pl-7A4KU6XK.js} +45 -4
  52. package/dist/pl-7A4KU6XK.js.map +1 -0
  53. package/dist/{pt-26Y6JFG5.js → pt-6MGYP7Q5.js} +45 -4
  54. package/dist/pt-6MGYP7Q5.js.map +1 -0
  55. package/dist/{ro-C7UXFRWJ.js → ro-IROCHNIA.js} +45 -4
  56. package/dist/ro-IROCHNIA.js.map +1 -0
  57. package/dist/{sv-44DVD76T.js → sv-WDJD75CV.js} +45 -4
  58. package/dist/sv-WDJD75CV.js.map +1 -0
  59. package/dist/test-utils.js +5 -5
  60. package/dist/test-utils.js.map +1 -1
  61. package/dist/{th-GIQRTBOY.js → th-RIO2E6B3.js} +45 -4
  62. package/dist/th-RIO2E6B3.js.map +1 -0
  63. package/dist/{tr-WMQWO4D6.js → tr-TZ7ERCIN.js} +45 -4
  64. package/dist/tr-TZ7ERCIN.js.map +1 -0
  65. package/dist/{uk-I7ML6RGG.js → uk-ZFWDKLUE.js} +45 -4
  66. package/dist/uk-ZFWDKLUE.js.map +1 -0
  67. package/dist/{vi-FGWECASQ.js → vi-UIOSP2JG.js} +45 -4
  68. package/dist/vi-UIOSP2JG.js.map +1 -0
  69. package/dist/{zh-L5FN73XC.js → zh-6R6YUY46.js} +45 -4
  70. package/dist/zh-6R6YUY46.js.map +1 -0
  71. package/package.json +23 -7
  72. package/src/components/AnnotateReferencesProgressWidget.tsx +35 -16
  73. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +41 -91
  74. package/src/components/modals/ConfigureGatherStep.tsx +102 -0
  75. package/src/components/modals/ContextSummary.tsx +15 -6
  76. package/src/components/modals/GatherContextStep.tsx +104 -59
  77. package/src/components/modals/ResourceGenerateModal.tsx +258 -0
  78. package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +82 -0
  79. package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +107 -0
  80. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +197 -0
  81. package/src/components/resource/panels/ReferencesPanel.tsx +10 -0
  82. package/src/components/resource/panels/ResourceInfoPanel.tsx +22 -0
  83. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +26 -0
  84. package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +14 -0
  85. package/src/features/admin-exchange/state/exchange-state-unit.ts +4 -2
  86. package/src/features/admin-security/state/__tests__/admin-security-state-unit.test.ts +14 -0
  87. package/src/features/admin-security/state/admin-security-state-unit.ts +4 -2
  88. package/src/features/admin-users/state/__tests__/admin-users-state-unit.test.ts +12 -5
  89. package/src/features/admin-users/state/admin-users-state-unit.ts +4 -2
  90. package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +11 -0
  91. package/src/features/auth-welcome/state/welcome-state-unit.ts +1 -1
  92. package/src/features/moderate-entity-tags/state/__tests__/entity-tags-state-unit.test.ts +14 -0
  93. package/src/features/moderate-entity-tags/state/entity-tags-state-unit.ts +4 -2
  94. package/src/features/resource-compose/components/ResourceComposePage.tsx +13 -7
  95. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +13 -0
  96. package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -2
  97. package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +15 -9
  98. package/src/features/resource-discovery/state/discover-state-unit.ts +5 -2
  99. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +79 -0
  100. package/src/features/resource-viewer/state/__tests__/resource-loader-state-unit.test.ts +11 -0
  101. package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +30 -0
  102. package/src/features/resource-viewer/state/resource-loader-state-unit.ts +1 -1
  103. package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +5 -2
  104. package/translations/ar.json +41 -2
  105. package/translations/bn.json +41 -2
  106. package/translations/cs.json +41 -2
  107. package/translations/da.json +41 -2
  108. package/translations/de.json +41 -2
  109. package/translations/el.json +41 -2
  110. package/translations/en.json +39 -0
  111. package/translations/es.json +41 -2
  112. package/translations/fa.json +41 -2
  113. package/translations/fi.json +41 -2
  114. package/translations/fr.json +41 -2
  115. package/translations/he.json +41 -2
  116. package/translations/hi.json +41 -2
  117. package/translations/id.json +41 -2
  118. package/translations/it.json +41 -2
  119. package/translations/ja.json +41 -2
  120. package/translations/ko.json +41 -2
  121. package/translations/ms.json +41 -2
  122. package/translations/nl.json +41 -2
  123. package/translations/no.json +41 -2
  124. package/translations/pl.json +41 -2
  125. package/translations/pt.json +41 -2
  126. package/translations/ro.json +41 -2
  127. package/translations/sv.json +41 -2
  128. package/translations/th.json +41 -2
  129. package/translations/tr.json +41 -2
  130. package/translations/uk.json +41 -2
  131. package/translations/vi.json +41 -2
  132. package/translations/zh.json +41 -2
  133. package/dist/ar-WA47UUWA.js.map +0 -1
  134. package/dist/bn-5ANDRIU6.js.map +0 -1
  135. package/dist/chunk-GERSK2DY.js.map +0 -1
  136. package/dist/cs-3RU7F4JX.js.map +0 -1
  137. package/dist/da-GSW5P5HG.js.map +0 -1
  138. package/dist/de-JUAUYF4Z.js.map +0 -1
  139. package/dist/el-JNLWCKEC.js.map +0 -1
  140. package/dist/es-WPOX225H.js.map +0 -1
  141. package/dist/fa-TAEP6N77.js.map +0 -1
  142. package/dist/fi-ZVZHANSP.js.map +0 -1
  143. package/dist/fr-VLZW7M4N.js.map +0 -1
  144. package/dist/he-QFAFYA77.js.map +0 -1
  145. package/dist/hi-AO3DQPCV.js.map +0 -1
  146. package/dist/id-GTXF42WM.js.map +0 -1
  147. package/dist/it-AS5GM232.js.map +0 -1
  148. package/dist/ja-GZ4HLUOF.js.map +0 -1
  149. package/dist/ko-A4XUXFGJ.js.map +0 -1
  150. package/dist/ms-YBAO3S6K.js.map +0 -1
  151. package/dist/nl-3TJGIIIU.js.map +0 -1
  152. package/dist/no-4AXIQPRQ.js.map +0 -1
  153. package/dist/pl-5GP6GKXO.js.map +0 -1
  154. package/dist/pt-26Y6JFG5.js.map +0 -1
  155. package/dist/ro-C7UXFRWJ.js.map +0 -1
  156. package/dist/sv-44DVD76T.js.map +0 -1
  157. package/dist/th-GIQRTBOY.js.map +0 -1
  158. package/dist/tr-WMQWO4D6.js.map +0 -1
  159. package/dist/uk-I7ML6RGG.js.map +0 -1
  160. package/dist/vi-FGWECASQ.js.map +0 -1
  161. package/dist/zh-L5FN73XC.js.map +0 -1
  162. /package/dist/{PdfAnnotationCanvas.client-U4EVBZEV.js.map → PdfAnnotationCanvas.client-P37SXBWQ.js.map} +0 -0
  163. /package/dist/{chunk-WHUGODTB.js.map → chunk-5LFFAVDI.js.map} +0 -0
  164. /package/dist/{chunk-YBGN4ACY.js.map → chunk-O23TLLXW.js.map} +0 -0
  165. /package/dist/{en-RBN2GUHF.js.map → en-P2L4X27N.js.map} +0 -0
@@ -0,0 +1,258 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
5
+ import { useResourceGather } from '../../hooks/useResourceGather';
6
+ import { useSemiont } from '../../session/SemiontProvider';
7
+ import { useObservable } from '../../hooks/useObservable';
8
+ import { ConfigureGatherStep, type ResourceGatherConfig } from './ConfigureGatherStep';
9
+ import { GatherContextStep } from './GatherContextStep';
10
+ import { ConfigureGenerationStep, type GenerationConfig } from './ConfigureGenerationStep';
11
+
12
+ export interface ResourceGenerateModalTranslations {
13
+ // Step titles + nav
14
+ gatherTitle: string;
15
+ reviewTitle: string;
16
+ configureTitle: string;
17
+ next: string;
18
+ back: string;
19
+ cancel: string;
20
+ // ConfigureGatherStep
21
+ gatherIntro: string;
22
+ includeContent: string;
23
+ includeSummary: string;
24
+ gatherDepth: string;
25
+ gatherMaxResources: string;
26
+ gatherButton: string;
27
+ excludeLabel: string;
28
+ // GatherContextStep display
29
+ loadingContext: string;
30
+ failedContext: string;
31
+ sourceContextLabel: string;
32
+ connectionsLabel: string;
33
+ citedByLabel: string;
34
+ // ConfigureGenerationStep
35
+ resourceTitle: string;
36
+ resourceTitlePlaceholder: string;
37
+ additionalInstructions: string;
38
+ additionalInstructionsPlaceholder: string;
39
+ language: string;
40
+ languageHelp: string;
41
+ creativity: string;
42
+ creativityFocused: string;
43
+ creativityCreative: string;
44
+ maxLength: string;
45
+ maxLengthHelp: string;
46
+ generate: string;
47
+ }
48
+
49
+ export interface ResourceGenerateModalProps {
50
+ isOpen: boolean;
51
+ onClose: () => void;
52
+ resourceId: string;
53
+ defaultTitle: string;
54
+ locale: string;
55
+ gatherDefaults?: Partial<ResourceGatherConfig>;
56
+ /**
57
+ * Emit the chosen generation config. The parent runs the job
58
+ * (`client.yield.fromResource(resourceId, …).run(…)`) — mirrors how the
59
+ * annotation wizard delegates `yield.fromAnnotation` to its parent.
60
+ */
61
+ onGenerateSubmit: (resourceId: string, config: GenerationConfig) => void;
62
+ translations: ResourceGenerateModalTranslations;
63
+ }
64
+
65
+ type Step = 'configure-gather' | 'review' | 'configure-generation';
66
+
67
+ /**
68
+ * Resource-generate flow (GENERATE-FROM-BUTTON): configure gather options →
69
+ * `gather.resource` → review the gathered `GatheredContext` → configure
70
+ * generation → emit. Reuses the kind-aware `GatherContextStep` for the review.
71
+ */
72
+ export function ResourceGenerateModal({
73
+ isOpen,
74
+ onClose,
75
+ resourceId,
76
+ defaultTitle,
77
+ locale,
78
+ gatherDefaults,
79
+ onGenerateSubmit,
80
+ translations: t,
81
+ }: ResourceGenerateModalProps) {
82
+ const [step, setStep] = useState<Step>('configure-gather');
83
+ const { context, loading, error, gather, reset } = useResourceGather();
84
+ const client = useObservable(useSemiont().activeSession$)?.client;
85
+ const entityTypeOptions = useObservable(client?.browse.entityTypes()) ?? [];
86
+ const [excludeEntityTypes, setExcludeEntityTypes] = useState<string[]>([]);
87
+
88
+ // Reset to the first step whenever the modal (re)opens.
89
+ useEffect(() => {
90
+ if (isOpen) {
91
+ setStep('configure-gather');
92
+ setExcludeEntityTypes([]);
93
+ reset();
94
+ }
95
+ }, [isOpen, reset]);
96
+
97
+ const handleGather = useCallback((config: ResourceGatherConfig) => {
98
+ setStep('review');
99
+ void gather(resourceId, {
100
+ ...config,
101
+ ...(excludeEntityTypes.length ? { excludeEntityTypes } : {}),
102
+ });
103
+ }, [gather, resourceId, excludeEntityTypes]);
104
+
105
+ const handleGenerate = useCallback((config: GenerationConfig) => {
106
+ onGenerateSubmit(resourceId, config);
107
+ onClose();
108
+ }, [onGenerateSubmit, resourceId, onClose]);
109
+
110
+ const stepTitle = step === 'configure-gather' ? t.gatherTitle : step === 'review' ? t.reviewTitle : t.configureTitle;
111
+
112
+ return (
113
+ <Transition appear show={isOpen}>
114
+ <Dialog as="div" className="semiont-search-modal" onClose={onClose}>
115
+ <TransitionChild
116
+ enter="ease-out duration-200"
117
+ enterFrom="opacity-0"
118
+ enterTo="opacity-100"
119
+ leave="ease-in duration-150"
120
+ leaveFrom="opacity-100"
121
+ leaveTo="opacity-0"
122
+ >
123
+ <div className="semiont-search-modal__backdrop" />
124
+ </TransitionChild>
125
+
126
+ <div className="semiont-search-modal__wrapper">
127
+ <div className="semiont-search-modal__centering semiont-search-modal__centering--center">
128
+ <TransitionChild
129
+ enter="ease-out duration-200"
130
+ enterFrom="opacity-0 scale-95"
131
+ enterTo="opacity-100 scale-100"
132
+ leave="ease-in duration-150"
133
+ leaveFrom="opacity-100 scale-100"
134
+ leaveTo="opacity-0 scale-95"
135
+ >
136
+ <DialogPanel className="semiont-search-modal__panel semiont-search-modal__panel--with-border semiont-search-modal__panel--gather">
137
+ <div className="semiont-search-modal__header">
138
+ <DialogTitle className="semiont-search-modal__title">{stepTitle}</DialogTitle>
139
+ <button onClick={onClose} className="semiont-search-modal__close-button" aria-label="Close">
140
+
141
+ </button>
142
+ </div>
143
+
144
+ {step === 'configure-gather' && (
145
+ <ConfigureGatherStep
146
+ defaults={gatherDefaults}
147
+ onGather={handleGather}
148
+ onCancel={onClose}
149
+ translations={{
150
+ intro: t.gatherIntro,
151
+ includeContent: t.includeContent,
152
+ includeSummary: t.includeSummary,
153
+ depth: t.gatherDepth,
154
+ maxResources: t.gatherMaxResources,
155
+ cancel: t.cancel,
156
+ gather: t.gatherButton,
157
+ }}
158
+ >
159
+ {entityTypeOptions.length > 0 && (
160
+ <div className="semiont-form__field semiont-form__entity-types">
161
+ <label className="semiont-form__label">{t.excludeLabel}</label>
162
+ <div className="semiont-form__entity-type-buttons">
163
+ {entityTypeOptions.map((et) => {
164
+ const isSelected = excludeEntityTypes.includes(et);
165
+ return (
166
+ <button
167
+ key={et}
168
+ type="button"
169
+ className="semiont-form__entity-type-button"
170
+ data-selected={isSelected}
171
+ aria-pressed={isSelected}
172
+ onClick={() => setExcludeEntityTypes(prev => prev.includes(et) ? prev.filter(x => x !== et) : [...prev, et])}
173
+ >
174
+ {et}
175
+ </button>
176
+ );
177
+ })}
178
+ </div>
179
+ </div>
180
+ )}
181
+ </ConfigureGatherStep>
182
+ )}
183
+
184
+ {step === 'review' && (
185
+ <>
186
+ <GatherContextStep
187
+ context={context}
188
+ contextLoading={loading}
189
+ contextError={error}
190
+ translations={{
191
+ title: '',
192
+ loadingContext: t.loadingContext,
193
+ failedContext: t.failedContext,
194
+ search: '',
195
+ generate: '',
196
+ compose: '',
197
+ resolutionStrategyLabel: '',
198
+ sourceContextLabel: t.sourceContextLabel,
199
+ connectionsLabel: t.connectionsLabel,
200
+ citedByLabel: t.citedByLabel,
201
+ userHintLabel: '',
202
+ userHintPlaceholder: '',
203
+ }}
204
+ />
205
+ <div className="semiont-modal__actions" style={{ paddingTop: '0.5rem' }}>
206
+ <button
207
+ type="button"
208
+ onClick={() => setStep('configure-gather')}
209
+ className="semiont-button--secondary semiont-button--flex"
210
+ >
211
+ ◀ {t.back}
212
+ </button>
213
+ <button
214
+ type="button"
215
+ onClick={() => setStep('configure-generation')}
216
+ disabled={!context}
217
+ className="semiont-button--primary semiont-button--flex"
218
+ >
219
+ {t.next} ▶
220
+ </button>
221
+ </div>
222
+ </>
223
+ )}
224
+
225
+ {step === 'configure-generation' && context && (
226
+ <ConfigureGenerationStep
227
+ defaultTitle={defaultTitle}
228
+ locale={locale}
229
+ context={context}
230
+ onBack={() => setStep('review')}
231
+ onCancel={onClose}
232
+ onGenerate={handleGenerate}
233
+ translations={{
234
+ resourceTitle: t.resourceTitle,
235
+ resourceTitlePlaceholder: t.resourceTitlePlaceholder,
236
+ additionalInstructions: t.additionalInstructions,
237
+ additionalInstructionsPlaceholder: t.additionalInstructionsPlaceholder,
238
+ language: t.language,
239
+ languageHelp: t.languageHelp,
240
+ creativity: t.creativity,
241
+ creativityFocused: t.creativityFocused,
242
+ creativityCreative: t.creativityCreative,
243
+ maxLength: t.maxLength,
244
+ maxLengthHelp: t.maxLengthHelp,
245
+ cancel: t.cancel,
246
+ back: t.back,
247
+ generate: t.generate,
248
+ }}
249
+ />
250
+ )}
251
+ </DialogPanel>
252
+ </TransitionChild>
253
+ </div>
254
+ </div>
255
+ </Dialog>
256
+ </Transition>
257
+ );
258
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * GENERATE-FROM-BUTTON P2/P4 — the gather-options form.
3
+ *
4
+ * Pure presentational form (no providers): it owns the gather config in local
5
+ * state and emits a `ResourceGatherConfig` on submit. The `children` slot hosts
6
+ * the Phase-4 exclusion multi-select.
7
+ */
8
+ import { describe, it, expect, vi } from 'vitest';
9
+ import { render, screen, fireEvent } from '@testing-library/react';
10
+ import '@testing-library/jest-dom';
11
+ import { ConfigureGatherStep } from '../ConfigureGatherStep';
12
+
13
+ const t = {
14
+ intro: 'Choose what to include.',
15
+ includeContent: 'Include content',
16
+ includeSummary: 'Include summary',
17
+ depth: 'Depth',
18
+ maxResources: 'Max resources',
19
+ cancel: 'Cancel',
20
+ gather: 'Gather',
21
+ };
22
+
23
+ describe('ConfigureGatherStep', () => {
24
+ it('renders the intro and the default option values', () => {
25
+ render(<ConfigureGatherStep onGather={vi.fn()} onCancel={vi.fn()} translations={t} />);
26
+ expect(screen.getByText('Choose what to include.')).toBeInTheDocument();
27
+ expect(screen.getByLabelText('Include content')).toBeChecked();
28
+ expect(screen.getByLabelText('Include summary')).toBeChecked();
29
+ expect(screen.getByLabelText('Depth')).toHaveValue(2);
30
+ expect(screen.getByLabelText('Max resources')).toHaveValue(10);
31
+ });
32
+
33
+ it('emits the default config on submit', () => {
34
+ const onGather = vi.fn();
35
+ render(<ConfigureGatherStep onGather={onGather} onCancel={vi.fn()} translations={t} />);
36
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
37
+ expect(onGather).toHaveBeenCalledWith({ includeContent: true, includeSummary: true, depth: 2, maxResources: 10 });
38
+ });
39
+
40
+ it('reflects edits in the emitted config', () => {
41
+ const onGather = vi.fn();
42
+ render(<ConfigureGatherStep onGather={onGather} onCancel={vi.fn()} translations={t} />);
43
+ fireEvent.click(screen.getByLabelText('Include content')); // uncheck
44
+ fireEvent.click(screen.getByLabelText('Include summary')); // uncheck
45
+ fireEvent.change(screen.getByLabelText('Depth'), { target: { value: '4' } });
46
+ fireEvent.change(screen.getByLabelText('Max resources'), { target: { value: '25' } });
47
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
48
+ expect(onGather).toHaveBeenCalledWith({ includeContent: false, includeSummary: false, depth: 4, maxResources: 25 });
49
+ });
50
+
51
+ it('honors the defaults prop', () => {
52
+ const onGather = vi.fn();
53
+ render(
54
+ <ConfigureGatherStep
55
+ defaults={{ includeSummary: false, depth: 1, maxResources: 5 }}
56
+ onGather={onGather}
57
+ onCancel={vi.fn()}
58
+ translations={t}
59
+ />,
60
+ );
61
+ expect(screen.getByLabelText('Include summary')).not.toBeChecked();
62
+ expect(screen.getByLabelText('Depth')).toHaveValue(1);
63
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
64
+ expect(onGather).toHaveBeenCalledWith({ includeContent: true, includeSummary: false, depth: 1, maxResources: 5 });
65
+ });
66
+
67
+ it('calls onCancel from the cancel button', () => {
68
+ const onCancel = vi.fn();
69
+ render(<ConfigureGatherStep onGather={vi.fn()} onCancel={onCancel} translations={t} />);
70
+ fireEvent.click(screen.getByRole('button', { name: /Cancel/ }));
71
+ expect(onCancel).toHaveBeenCalledTimes(1);
72
+ });
73
+
74
+ it('renders the children slot (exclusion picker)', () => {
75
+ render(
76
+ <ConfigureGatherStep onGather={vi.fn()} onCancel={vi.fn()} translations={t}>
77
+ <div>EXCLUDE-SLOT</div>
78
+ </ConfigureGatherStep>,
79
+ );
80
+ expect(screen.getByText('EXCLUDE-SLOT')).toBeInTheDocument();
81
+ });
82
+ });
@@ -0,0 +1,107 @@
1
+ /**
2
+ * GENERATE-FROM-BUTTON P1 — the GatheredContext display renders a resource focus.
3
+ *
4
+ * ContextSummary's graph views are focus-agnostic; GatherContextStep gains a
5
+ * resource-focus strip and hides the annotation-only controls (hint + the
6
+ * Bind/Generate/Compose footer) for a resource focus.
7
+ */
8
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
9
+ import { render } from '@testing-library/react';
10
+ import type { GatheredContext } from '@semiont/core';
11
+ import { GatherContextStep } from '../GatherContextStep';
12
+ import { ContextSummary } from '../ContextSummary';
13
+
14
+ const t = {
15
+ title: 'Gather',
16
+ loadingContext: 'Loading…',
17
+ failedContext: 'Failed',
18
+ search: 'Search',
19
+ generate: 'Generate',
20
+ compose: 'Compose',
21
+ resolutionStrategyLabel: 'Strategy',
22
+ sourceContextLabel: 'Source',
23
+ connectionsLabel: 'Connections',
24
+ citedByLabel: 'Cited by',
25
+ userHintLabel: 'Hint',
26
+ userHintPlaceholder: 'hint…',
27
+ };
28
+
29
+ function resourceContext(): GatheredContext {
30
+ return {
31
+ focus: {
32
+ kind: 'resource',
33
+ resource: { id: 'res-1', name: 'My Resource' },
34
+ summary: 'A short summary',
35
+ suggestedReferences: ['Suggested Topic'],
36
+ content: { main: 'main content' },
37
+ },
38
+ graph: {
39
+ nodes: [
40
+ { id: 'res-1', type: 'resource', label: 'My Resource' },
41
+ { id: 'res-2', type: 'resource', label: 'Related Resource', entityTypes: ['Topic'] },
42
+ ],
43
+ edges: [{ source: 'res-1', target: 'res-2', type: 'peer' }],
44
+ },
45
+ metadata: { entityTypes: ['Topic'] },
46
+ } as unknown as GatheredContext;
47
+ }
48
+
49
+ describe('GatheredContext display — resource focus', () => {
50
+ beforeEach(() => {
51
+ // jsdom doesn't implement scrollIntoView — GatherContextStep's annotation
52
+ // strip calls it on mount. (Same stub the panel tests use.)
53
+ Element.prototype.scrollIntoView = vi.fn();
54
+ });
55
+
56
+ it('ContextSummary renders graph views (connections) for a resource focus', () => {
57
+ const { container } = render(<ContextSummary context={resourceContext()} translations={t} />);
58
+ expect(container.textContent).toContain('Related Resource'); // peer connection from deriveViews
59
+ });
60
+
61
+ it('GatherContextStep shows the resource strip and hides the annotation-only footer', () => {
62
+ const { container } = render(
63
+ <GatherContextStep
64
+ context={resourceContext()}
65
+ contextLoading={false}
66
+ contextError={null}
67
+ translations={t}
68
+ />,
69
+ );
70
+ expect(container.textContent).toContain('My Resource'); // focal resource name
71
+ expect(container.textContent).toContain('A short summary'); // resource summary
72
+ expect(container.textContent).toContain('Suggested Topic'); // suggestedReferences chip
73
+ expect(container.textContent).toContain('Related Resource'); // graph view via ContextSummary
74
+ // annotation-only controls are gated out for a resource focus
75
+ expect(container.textContent).not.toContain('Strategy');
76
+ expect(container.querySelector('.semiont-gather__footer')).toBeNull();
77
+ expect(container.querySelector('.semiont-gather__hint-textarea')).toBeNull();
78
+ });
79
+
80
+ it('GatherContextStep still shows the footer for an annotation focus', () => {
81
+ const annotationContext = {
82
+ focus: {
83
+ kind: 'annotation',
84
+ annotation: { id: 'anno-1', motivation: 'linking' },
85
+ sourceResource: { id: 'res-1', name: 'Host' },
86
+ selected: { before: 'a ', text: 'term', after: ' b' },
87
+ },
88
+ graph: { nodes: [], edges: [] },
89
+ metadata: {},
90
+ } as unknown as GatheredContext;
91
+ const { container } = render(
92
+ <GatherContextStep
93
+ context={annotationContext}
94
+ contextLoading={false}
95
+ contextError={null}
96
+ userHint=""
97
+ onUserHintChange={() => {}}
98
+ onBind={() => {}}
99
+ onGenerate={() => {}}
100
+ onCompose={() => {}}
101
+ translations={t}
102
+ />,
103
+ );
104
+ expect(container.querySelector('.semiont-gather__footer')).not.toBeNull();
105
+ expect(container.textContent).toContain('Strategy');
106
+ });
107
+ });
@@ -0,0 +1,197 @@
1
+ /**
2
+ * GENERATE-FROM-BUTTON P2/P4 — the resource-generate flow modal.
3
+ *
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.
8
+ */
9
+ import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
10
+ import { render, screen, fireEvent } from '@testing-library/react';
11
+ import '@testing-library/jest-dom';
12
+ import { BehaviorSubject, of } from 'rxjs';
13
+ import type { GatheredContext } from '@semiont/core';
14
+ import type { GenerationConfig } from '../ConfigureGenerationStep';
15
+
16
+ const RESOURCE_CONTEXT = {
17
+ focus: { kind: 'resource', resource: { id: 'res-1', name: 'My Resource' }, summary: 'A short summary' },
18
+ graph: { nodes: [{ id: 'res-1', type: 'resource', label: 'My Resource' }], edges: [] },
19
+ metadata: {},
20
+ } as unknown as GatheredContext;
21
+
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
+ // Stable observable instance — the modal calls `entityTypes()` inline in
40
+ // render, so a fresh observable each call would re-subscribe every render and
41
+ // loop. The real SDK returns a cached observable; mirror that here.
42
+ const entityTypes$ = of<string[]>(['Person', 'Topic']);
43
+ const mockClient = { browse: { entityTypes: () => entityTypes$ } };
44
+ const activeSession$ = new BehaviorSubject<unknown>({ client: mockClient });
45
+ vi.mock('../../../session/SemiontProvider', async () => {
46
+ const actual = await vi.importActual<typeof import('../../../session/SemiontProvider')>('../../../session/SemiontProvider');
47
+ return { ...actual, useSemiont: () => ({ activeSession$ }) };
48
+ });
49
+
50
+ import { ResourceGenerateModal } from '../ResourceGenerateModal';
51
+
52
+ const T = {
53
+ gatherTitle: 'Configure Gather',
54
+ reviewTitle: 'Review Context',
55
+ configureTitle: 'Configure Generation',
56
+ next: 'Next',
57
+ back: 'Back',
58
+ cancel: 'Cancel',
59
+ gatherIntro: 'Choose what to include.',
60
+ includeContent: 'Include content',
61
+ includeSummary: 'Include summary',
62
+ gatherDepth: 'Depth',
63
+ gatherMaxResources: 'Max resources',
64
+ gatherButton: 'Gather',
65
+ excludeLabel: 'Exclude from recall',
66
+ loadingContext: 'Gathering…',
67
+ failedContext: 'Failed',
68
+ sourceContextLabel: 'Resource',
69
+ connectionsLabel: 'Connections',
70
+ citedByLabel: 'Cited by',
71
+ resourceTitle: 'New resource title',
72
+ resourceTitlePlaceholder: 'Title…',
73
+ additionalInstructions: 'Additional Instructions',
74
+ additionalInstructionsPlaceholder: 'Optional…',
75
+ language: 'Language',
76
+ languageHelp: 'Language help',
77
+ creativity: 'Creativity',
78
+ creativityFocused: 'Focused',
79
+ creativityCreative: 'Creative',
80
+ maxLength: 'Max Length',
81
+ maxLengthHelp: 'Max help',
82
+ generate: 'Generate',
83
+ };
84
+
85
+ let onClose: Mock<() => void>;
86
+ let onGenerateSubmit: Mock<(resourceId: string, config: GenerationConfig) => void>;
87
+
88
+ function renderModal(props: Partial<React.ComponentProps<typeof ResourceGenerateModal>> = {}) {
89
+ return render(
90
+ <ResourceGenerateModal
91
+ isOpen
92
+ onClose={onClose}
93
+ resourceId="res-1"
94
+ defaultTitle="Default Title"
95
+ locale="en"
96
+ onGenerateSubmit={onGenerateSubmit}
97
+ translations={T}
98
+ {...props}
99
+ />,
100
+ );
101
+ }
102
+
103
+ describe('ResourceGenerateModal', () => {
104
+ beforeEach(() => {
105
+ vi.clearAllMocks();
106
+ onClose = vi.fn<() => void>();
107
+ onGenerateSubmit = vi.fn<(resourceId: string, config: GenerationConfig) => void>();
108
+ h.state = { context: null, loading: false, error: null };
109
+ activeSession$.next({ client: mockClient });
110
+ // jsdom doesn't implement scrollIntoView; GatherContextStep may call it.
111
+ Element.prototype.scrollIntoView = vi.fn();
112
+ });
113
+
114
+ it('renders nothing when closed', () => {
115
+ renderModal({ isOpen: false });
116
+ expect(screen.queryByText('Configure Gather')).not.toBeInTheDocument();
117
+ });
118
+
119
+ it('opens on the configure-gather step with the exclusion options', () => {
120
+ renderModal();
121
+ expect(screen.getByText('Configure Gather')).toBeInTheDocument(); // step title
122
+ expect(screen.getByText('Choose what to include.')).toBeInTheDocument();
123
+ expect(screen.getByLabelText('Include content')).toBeInTheDocument();
124
+ expect(screen.getByRole('button', { name: 'Person' })).toBeInTheDocument();
125
+ expect(screen.getByRole('button', { name: 'Topic' })).toBeInTheDocument();
126
+ });
127
+
128
+ it('submitting the gather step calls gather() and advances to review (exclusion omitted when none picked)', () => {
129
+ renderModal();
130
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
131
+ expect(h.gather).toHaveBeenCalledWith('res-1', { includeContent: true, includeSummary: true, depth: 2, maxResources: 10 });
132
+ expect(screen.getByText('Review Context')).toBeInTheDocument(); // step title flipped
133
+ expect(screen.getByRole('button', { name: /Next/ })).toBeDisabled(); // no context yet
134
+ });
135
+
136
+ it('threads picked entity types into the gather call', () => {
137
+ renderModal();
138
+ fireEvent.click(screen.getByRole('button', { name: 'Person' })); // select to exclude
139
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
140
+ expect(h.gather).toHaveBeenCalledWith('res-1', {
141
+ includeContent: true,
142
+ includeSummary: true,
143
+ depth: 2,
144
+ maxResources: 10,
145
+ excludeEntityTypes: ['Person'],
146
+ });
147
+ });
148
+
149
+ it('walks gather → review → configure-generation → emits onGenerateSubmit then closes', () => {
150
+ h.state.context = RESOURCE_CONTEXT; // gather already resolved
151
+ renderModal();
152
+
153
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ })); // → review
154
+ const next = screen.getByRole('button', { name: /Next/ });
155
+ expect(next).toBeEnabled();
156
+ fireEvent.click(next); // → configure-generation
157
+
158
+ fireEvent.change(screen.getByLabelText('New resource title'), { target: { value: 'Generated Doc' } });
159
+ fireEvent.change(screen.getByLabelText('Save location'), { target: { value: 'generated/out.md' } });
160
+ fireEvent.click(screen.getByRole('button', { name: /Generate/ }));
161
+
162
+ expect(onGenerateSubmit).toHaveBeenCalledTimes(1);
163
+ expect(onGenerateSubmit).toHaveBeenCalledWith(
164
+ 'res-1',
165
+ expect.objectContaining({
166
+ title: 'Generated Doc',
167
+ storagePath: 'file://generated/out.md',
168
+ context: RESOURCE_CONTEXT,
169
+ }),
170
+ );
171
+ expect(onClose).toHaveBeenCalled();
172
+ });
173
+
174
+ it('Back from configure-generation returns to review', () => {
175
+ h.state.context = RESOURCE_CONTEXT;
176
+ renderModal();
177
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ })); // → review
178
+ fireEvent.click(screen.getByRole('button', { name: /Next/ })); // → configure-generation
179
+ expect(screen.getByText('Configure Generation')).toBeInTheDocument();
180
+ fireEvent.click(screen.getByRole('button', { name: /Back/ })); // → review
181
+ expect(screen.getByText('Review Context')).toBeInTheDocument();
182
+ });
183
+
184
+ it('Back from review returns to the configure-gather step', () => {
185
+ renderModal();
186
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ })); // → review
187
+ expect(screen.getByText('Review Context')).toBeInTheDocument();
188
+ fireEvent.click(screen.getByRole('button', { name: /Back/ })); // → configure-gather
189
+ expect(screen.getByText('Configure Gather')).toBeInTheDocument();
190
+ });
191
+
192
+ it('the header close button calls onClose', () => {
193
+ renderModal();
194
+ fireEvent.click(screen.getByLabelText('Close'));
195
+ expect(onClose).toHaveBeenCalledTimes(1);
196
+ });
197
+ });
@@ -455,6 +455,16 @@ export function ReferencesPanel({
455
455
  <AnnotateReferencesProgressWidget
456
456
  progress={progress}
457
457
  annotationType="reference"
458
+ cancelJobType="annotation"
459
+ translations={{
460
+ title: t('annotationProgressTitle'),
461
+ cancel: t('cancelAnnotation'),
462
+ inProgress: t('annotating'),
463
+ complete: t('complete'),
464
+ failed: t('failed'),
465
+ found: (count) => t('found', { count }),
466
+ current: (entityType) => t('current', { entityType }),
467
+ }}
458
468
  />
459
469
  )}
460
470
  </>