@semiont/react-ui 0.5.26 → 0.5.27

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 (249) hide show
  1. package/README.md +28 -4
  2. package/dist/{PdfAnnotationCanvas.client-6ZSF6ZTJ.js → PdfAnnotationCanvas.client-EVUPTUWZ.js} +5 -5
  3. package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js.map +1 -0
  4. package/dist/{PdfAnnotationCanvas.client-JCJMXWWJ.css → PdfAnnotationCanvas.client-TGDPPGXJ.css} +6 -1
  5. package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css.map +1 -0
  6. package/dist/{ar-YTUUGK2O.js → ar-OU3DQGC6.js} +53 -7
  7. package/dist/ar-OU3DQGC6.js.map +1 -0
  8. package/dist/{bn-ASGNGPMD.js → bn-PHWCR6AZ.js} +53 -7
  9. package/dist/bn-PHWCR6AZ.js.map +1 -0
  10. package/dist/{chunk-OBQOZCJE.js → chunk-5VDTWRXJ.js} +31 -31
  11. package/dist/{chunk-L3WKRWP3.js → chunk-PCRRBU7H.js} +53 -7
  12. package/dist/chunk-PCRRBU7H.js.map +1 -0
  13. package/dist/{cs-WMYVAOZY.js → cs-NEXYZ62I.js} +53 -7
  14. package/dist/cs-NEXYZ62I.js.map +1 -0
  15. package/dist/{da-FSHHN4D6.js → da-FAT43YCU.js} +53 -7
  16. package/dist/da-FAT43YCU.js.map +1 -0
  17. package/dist/{de-FJFO3TPT.js → de-KRQ3WBCU.js} +53 -7
  18. package/dist/de-KRQ3WBCU.js.map +1 -0
  19. package/dist/{el-XOH4QIAG.js → el-Q4IU3EKC.js} +53 -7
  20. package/dist/el-Q4IU3EKC.js.map +1 -0
  21. package/dist/{en-3FCBW3YD.js → en-2KADE7N4.js} +4 -2
  22. package/dist/{es-HDX4QI7E.js → es-2IO3R2MK.js} +53 -7
  23. package/dist/es-2IO3R2MK.js.map +1 -0
  24. package/dist/{fa-DJ6WTFTJ.js → fa-Q5GOQFXX.js} +53 -7
  25. package/dist/fa-Q5GOQFXX.js.map +1 -0
  26. package/dist/{fi-4S7H7W7U.js → fi-R6NMRWBE.js} +53 -7
  27. package/dist/fi-R6NMRWBE.js.map +1 -0
  28. package/dist/{fr-6UJSYHXC.js → fr-QY4D5E6J.js} +53 -7
  29. package/dist/fr-QY4D5E6J.js.map +1 -0
  30. package/dist/{he-ZZJOHJX2.js → he-X6Y77XHY.js} +53 -7
  31. package/dist/he-X6Y77XHY.js.map +1 -0
  32. package/dist/{hi-HZJTAW7U.js → hi-XL7IOEQM.js} +53 -7
  33. package/dist/hi-XL7IOEQM.js.map +1 -0
  34. package/dist/{id-E3I6ILEM.js → id-QZ4ZZS67.js} +53 -7
  35. package/dist/id-QZ4ZZS67.js.map +1 -0
  36. package/dist/index.css +105 -454
  37. package/dist/index.css.map +1 -1
  38. package/dist/index.d.ts +155 -54
  39. package/dist/index.js +1798 -1637
  40. package/dist/index.js.map +1 -1
  41. package/dist/integrations/css-modules-helper.js +1 -3
  42. package/dist/integrations/css-modules-helper.js.map +1 -1
  43. package/dist/{it-ULAPAD7U.js → it-ME2Q7OB6.js} +53 -7
  44. package/dist/it-ME2Q7OB6.js.map +1 -0
  45. package/dist/{ja-SAHZQ4CZ.js → ja-HX7QASRI.js} +53 -7
  46. package/dist/ja-HX7QASRI.js.map +1 -0
  47. package/dist/{ko-DFU2ADQ4.js → ko-EQO5AHAV.js} +53 -7
  48. package/dist/ko-EQO5AHAV.js.map +1 -0
  49. package/dist/{ms-NWL76XNT.js → ms-UILFSF3I.js} +53 -7
  50. package/dist/ms-UILFSF3I.js.map +1 -0
  51. package/dist/{nl-ATCM76WN.js → nl-NNI45PMK.js} +53 -7
  52. package/dist/nl-NNI45PMK.js.map +1 -0
  53. package/dist/{no-UIAQ5RJF.js → no-MAL5F6P6.js} +53 -7
  54. package/dist/no-MAL5F6P6.js.map +1 -0
  55. package/dist/{pl-NOECQDXD.js → pl-4RMY6JAJ.js} +53 -7
  56. package/dist/pl-4RMY6JAJ.js.map +1 -0
  57. package/dist/{pt-SVWIIUPC.js → pt-VQH2X3QQ.js} +53 -7
  58. package/dist/pt-VQH2X3QQ.js.map +1 -0
  59. package/dist/{ro-NR2DRTZG.js → ro-XXCQO2OG.js} +53 -7
  60. package/dist/ro-XXCQO2OG.js.map +1 -0
  61. package/dist/{sv-STVJ37JP.js → sv-7Z6EJ6U4.js} +53 -7
  62. package/dist/sv-7Z6EJ6U4.js.map +1 -0
  63. package/dist/test-utils.css +1 -1
  64. package/dist/test-utils.css.map +1 -1
  65. package/dist/test-utils.d.ts +1 -1
  66. package/dist/test-utils.js +2 -2
  67. package/dist/test-utils.js.map +1 -1
  68. package/dist/{th-UWQR4K54.js → th-L3DXXYVQ.js} +53 -7
  69. package/dist/th-L3DXXYVQ.js.map +1 -0
  70. package/dist/{tr-WNXZSD27.js → tr-P3DFVLFJ.js} +53 -7
  71. package/dist/tr-P3DFVLFJ.js.map +1 -0
  72. package/dist/{uk-SJRMB47Y.js → uk-7C5D3PXS.js} +53 -7
  73. package/dist/uk-7C5D3PXS.js.map +1 -0
  74. package/dist/{vi-TMRQFES2.js → vi-ZLTLEO7P.js} +53 -7
  75. package/dist/vi-ZLTLEO7P.js.map +1 -0
  76. package/dist/{zh-QOL3LYEM.js → zh-5FHVWDIN.js} +53 -7
  77. package/dist/zh-5FHVWDIN.js.map +1 -0
  78. package/package.json +7 -7
  79. package/src/components/AssistProgress.tsx +144 -134
  80. package/src/components/{EntityFoundLog.tsx → ItemFoundLog.tsx} +12 -7
  81. package/src/components/ProtectedErrorBoundary.css +3 -1
  82. package/src/components/StatusDisplay.css +1 -1
  83. package/src/components/Toast.css +3 -1
  84. package/src/components/__tests__/AssistProgress.test.tsx +216 -148
  85. package/src/components/annotation/annotation-entries.css +0 -4
  86. package/src/components/annotation/references.css +0 -4
  87. package/src/components/image-annotation/AnnotationOverlay.tsx +6 -6
  88. package/src/components/image-annotation/SvgDrawingCanvas.tsx +1 -1
  89. package/src/components/image-annotation/__tests__/AnnotationOverlay.click.test.tsx +7 -9
  90. package/src/components/image-annotation/__tests__/SvgDrawingCanvas.click.test.tsx +1 -2
  91. package/src/components/layout/Header.css +0 -4
  92. package/src/components/layout/LeftSidebar.css +0 -4
  93. package/src/components/modals/ConfigureGenerationStep.tsx +109 -48
  94. package/src/components/modals/ConfigureSearchStep.tsx +23 -40
  95. package/src/components/modals/ReferenceWizardModal.tsx +55 -16
  96. package/src/components/modals/ResourceGenerateModal.tsx +24 -8
  97. package/src/components/modals/ResourceSearchModal.tsx +1 -1
  98. package/src/components/modals/SearchModal.css +0 -12
  99. package/src/components/modals/SearchModal.tsx +1 -1
  100. package/src/components/modals/SearchResultsStep.tsx +2 -20
  101. package/src/components/modals/WizardFooter.tsx +63 -0
  102. package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +255 -0
  103. package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +72 -0
  104. package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +256 -0
  105. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +1 -0
  106. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +6 -2
  107. package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +6 -2
  108. package/src/components/modals/__tests__/WizardFooter.test.tsx +235 -0
  109. package/src/components/modals/modals.css +16 -42
  110. package/src/components/navigation/NavigationTabs.css +0 -20
  111. package/src/components/pdf-annotation/PdfAnnotationCanvas.css +21 -2
  112. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +2 -2
  113. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +3 -5
  114. package/src/components/resource/AnnotateView.tsx +16 -5
  115. package/src/components/resource/AnnotationHistory.tsx +2 -0
  116. package/src/components/resource/BrowseView.tsx +15 -4
  117. package/src/components/resource/ResourceViewer.tsx +23 -24
  118. package/src/components/resource/__tests__/AnnotateView.focus.test.tsx +50 -0
  119. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +2 -0
  120. package/src/components/resource/__tests__/BrowseView.test.tsx +29 -3
  121. package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +47 -3
  122. package/src/components/resource/event-formatting.ts +0 -3
  123. package/src/components/resource/panels/AssessmentEntry.tsx +1 -1
  124. package/src/components/resource/panels/AssessmentPanel.css +5 -6
  125. package/src/components/resource/panels/AssistSection.css +68 -271
  126. package/src/components/resource/panels/AssistSection.tsx +4 -4
  127. package/src/components/resource/panels/AssistShell.tsx +12 -4
  128. package/src/components/resource/panels/CollaborationPanel.css +47 -22
  129. package/src/components/resource/panels/CollaborationPanel.tsx +93 -2
  130. package/src/components/resource/panels/CommentEntry.tsx +1 -1
  131. package/src/components/resource/panels/CommentsPanel.css +0 -16
  132. package/src/components/resource/panels/HighlightEntry.tsx +1 -1
  133. package/src/components/resource/panels/HighlightPanel.css +0 -7
  134. package/src/components/resource/panels/JsonLdPanel.css +0 -4
  135. package/src/components/resource/panels/ReferenceEntry.tsx +1 -1
  136. package/src/components/resource/panels/ReferencesPanel.css +0 -12
  137. package/src/components/resource/panels/ReferencesPanel.tsx +12 -20
  138. package/src/components/resource/panels/ResourceInfoPanel.css +0 -32
  139. package/src/components/resource/panels/ResourceInfoPanel.tsx +1 -1
  140. package/src/components/resource/panels/StatisticsPanel.css +0 -20
  141. package/src/components/resource/panels/TagEntry.tsx +1 -1
  142. package/src/components/resource/panels/TaggingPanel.css +0 -4
  143. package/src/components/resource/panels/TaggingPanel.tsx +2 -10
  144. package/src/components/resource/panels/UnifiedAnnotationsPanel.css +0 -4
  145. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +0 -1
  146. package/src/components/resource/panels/__tests__/AssistSection.test.tsx +48 -50
  147. package/src/components/resource/panels/__tests__/AssistShell.test.tsx +11 -6
  148. package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +89 -0
  149. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +1 -2
  150. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +0 -1
  151. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +44 -36
  152. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +0 -1
  153. package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +1 -1
  154. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +34 -53
  155. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +37 -1
  156. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +0 -1
  157. package/src/components/settings/SettingsPanel.css +0 -4
  158. package/src/features/auth/auth.css +2 -26
  159. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +25 -1
  160. package/src/features/resource-compose/state/compose-page-state-unit.ts +11 -2
  161. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +55 -0
  162. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +29 -0
  163. package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +53 -3
  164. package/src/features/resource-discovery/state/discover-state-unit.ts +29 -8
  165. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +41 -20
  166. package/src/features/resource-viewer/hooks/__tests__/useResourceViewedReport.test.tsx +63 -0
  167. package/src/features/resource-viewer/hooks/useResourceViewedReport.ts +33 -0
  168. package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +2 -2
  169. package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +1 -1
  170. package/src/styles/core/buttons.css +3 -1
  171. package/src/styles/core/forms.css +0 -21
  172. package/src/styles/core/indicators.css +0 -4
  173. package/src/styles/core/inputs.css +0 -4
  174. package/src/styles/core/progress.css +2 -34
  175. package/src/styles/core/sliders.css +0 -4
  176. package/src/styles/core/toggles.css +0 -4
  177. package/src/styles/features/resource-discovery.css +13 -0
  178. package/src/styles/features/resource-viewer.css +0 -20
  179. package/src/styles/motivations/motivation-assessment.css +2 -2
  180. package/src/styles/panels/history-panel.css +0 -40
  181. package/src/styles/panels/user-panel.css +0 -3
  182. package/src/styles/patterns/panel-helpers.css +0 -27
  183. package/src/styles/patterns/panel-sections.css +0 -59
  184. package/src/styles/patterns/panels-base.css +0 -32
  185. package/translations/ar.json +49 -5
  186. package/translations/bn.json +49 -5
  187. package/translations/cs.json +49 -5
  188. package/translations/da.json +49 -5
  189. package/translations/de.json +49 -5
  190. package/translations/el.json +49 -5
  191. package/translations/en.json +49 -5
  192. package/translations/es.json +49 -5
  193. package/translations/fa.json +49 -5
  194. package/translations/fi.json +49 -5
  195. package/translations/fr.json +49 -5
  196. package/translations/he.json +49 -5
  197. package/translations/hi.json +49 -5
  198. package/translations/id.json +49 -5
  199. package/translations/it.json +49 -5
  200. package/translations/ja.json +49 -5
  201. package/translations/ko.json +49 -5
  202. package/translations/ms.json +49 -5
  203. package/translations/nl.json +49 -5
  204. package/translations/no.json +49 -5
  205. package/translations/pl.json +49 -5
  206. package/translations/pt.json +49 -5
  207. package/translations/ro.json +49 -5
  208. package/translations/sv.json +49 -5
  209. package/translations/th.json +49 -5
  210. package/translations/tr.json +49 -5
  211. package/translations/uk.json +49 -5
  212. package/translations/vi.json +49 -5
  213. package/translations/zh.json +49 -5
  214. package/dist/PdfAnnotationCanvas.client-6ZSF6ZTJ.js.map +0 -1
  215. package/dist/PdfAnnotationCanvas.client-JCJMXWWJ.css.map +0 -1
  216. package/dist/ar-YTUUGK2O.js.map +0 -1
  217. package/dist/bn-ASGNGPMD.js.map +0 -1
  218. package/dist/chunk-L3WKRWP3.js.map +0 -1
  219. package/dist/chunk-XUDKYAVC.js +0 -21
  220. package/dist/chunk-XUDKYAVC.js.map +0 -1
  221. package/dist/cs-WMYVAOZY.js.map +0 -1
  222. package/dist/da-FSHHN4D6.js.map +0 -1
  223. package/dist/de-FJFO3TPT.js.map +0 -1
  224. package/dist/el-XOH4QIAG.js.map +0 -1
  225. package/dist/es-HDX4QI7E.js.map +0 -1
  226. package/dist/fa-DJ6WTFTJ.js.map +0 -1
  227. package/dist/fi-4S7H7W7U.js.map +0 -1
  228. package/dist/fr-6UJSYHXC.js.map +0 -1
  229. package/dist/he-ZZJOHJX2.js.map +0 -1
  230. package/dist/hi-HZJTAW7U.js.map +0 -1
  231. package/dist/id-E3I6ILEM.js.map +0 -1
  232. package/dist/it-ULAPAD7U.js.map +0 -1
  233. package/dist/ja-SAHZQ4CZ.js.map +0 -1
  234. package/dist/ko-DFU2ADQ4.js.map +0 -1
  235. package/dist/ms-NWL76XNT.js.map +0 -1
  236. package/dist/nl-ATCM76WN.js.map +0 -1
  237. package/dist/no-UIAQ5RJF.js.map +0 -1
  238. package/dist/pl-NOECQDXD.js.map +0 -1
  239. package/dist/pt-SVWIIUPC.js.map +0 -1
  240. package/dist/ro-NR2DRTZG.js.map +0 -1
  241. package/dist/sv-STVJ37JP.js.map +0 -1
  242. package/dist/th-UWQR4K54.js.map +0 -1
  243. package/dist/tr-WNXZSD27.js.map +0 -1
  244. package/dist/uk-SJRMB47Y.js.map +0 -1
  245. package/dist/vi-TMRQFES2.js.map +0 -1
  246. package/dist/zh-QOL3LYEM.js.map +0 -1
  247. package/src/integrations/tailwind-plugin.cjs +0 -224
  248. /package/dist/{chunk-OBQOZCJE.js.map → chunk-5VDTWRXJ.js.map} +0 -0
  249. /package/dist/{en-3FCBW3YD.js.map → en-2KADE7N4.js.map} +0 -0
@@ -74,7 +74,7 @@ export function SearchModal({
74
74
  (q) =>
75
75
  semiontRef.current!.browse.resources({ search: q, limit: SEARCH_LIMIT }).pipe(
76
76
  map((st) => {
77
- const resources = readyValue(st);
77
+ const resources = readyValue(st)?.resources;
78
78
  if (resources === undefined) return undefined;
79
79
  return resources
80
80
  .map((resource): SearchResult | null => {
@@ -1,4 +1,5 @@
1
1
  'use client';
2
+ import { WizardFooter } from './WizardFooter';
2
3
 
3
4
  import type { GatheredContext, ResourceDescriptor } from '@semiont/core';
4
5
  import { ContextSummary } from './ContextSummary';
@@ -14,12 +15,10 @@ export interface SearchResultsStepProps {
14
15
  context: GatheredContext;
15
16
  onLink: (resourceId: string) => void;
16
17
  onBack: () => void;
17
- onCancel: () => void;
18
18
  translations: {
19
19
  noResults: string;
20
20
  link: string;
21
21
  back: string;
22
- cancel: string;
23
22
  score: string;
24
23
  } & ContextSummaryTranslations;
25
24
  }
@@ -29,7 +28,6 @@ export function SearchResultsStep({
29
28
  context,
30
29
  onLink,
31
30
  onBack,
32
- onCancel,
33
31
  translations: t,
34
32
  }: SearchResultsStepProps) {
35
33
  return (
@@ -102,23 +100,7 @@ export function SearchResultsStep({
102
100
  </div>
103
101
  </div>
104
102
 
105
- {/* Action Buttons */}
106
- <div className="semiont-modal__actions" style={{ paddingTop: '0.5rem' }}>
107
- <button
108
- type="button"
109
- onClick={onCancel}
110
- className="semiont-button--secondary semiont-button--flex"
111
- >
112
- ✕ {t.cancel}
113
- </button>
114
- <button
115
- type="button"
116
- onClick={onBack}
117
- className="semiont-button--secondary semiont-button--flex"
118
- >
119
- ◀ {t.back}
120
- </button>
121
- </div>
103
+ <WizardFooter backLabel={t.back} onBack={onBack} />
122
104
  </>
123
105
  );
124
106
  }
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * The one wizard footer (WIZARD-NAVIGATION D7).
5
+ *
6
+ * Four steps used to hand-build this markup, which is how four different footers
7
+ * happened: three carried a `✕ Cancel` that duplicated the corner control, one had no
8
+ * footer at all, and every button stretched to equal width so "leave" read as a peer of
9
+ * "do the thing".
10
+ *
11
+ * The grammar, settled in D1/D2:
12
+ * • Dismissal is NOT here. The corner ✕, Esc and the backdrop are the way out, and
13
+ * they are the same on every step including the one with no footer.
14
+ * • Retreat is chrome: quiet, left, no emoji.
15
+ * • Advance is the decision: filled, right, one verb.
16
+ */
17
+
18
+ export interface WizardFooterPrimary {
19
+ label: string;
20
+ /** Shown while `pending`; falls back to `label`. */
21
+ pendingLabel?: string;
22
+ pending?: boolean;
23
+ type: 'submit' | 'button';
24
+ onClick?: () => void;
25
+ }
26
+
27
+ export interface WizardFooterProps {
28
+ /** Omit both to render no retreat — correct on the first step. */
29
+ backLabel?: string;
30
+ onBack?: () => void;
31
+ /** Omit on steps whose action lives elsewhere (search results links per row). */
32
+ primary?: WizardFooterPrimary;
33
+ }
34
+
35
+ export function WizardFooter({ backLabel, onBack, primary }: WizardFooterProps) {
36
+ const pending = primary?.pending ?? false;
37
+
38
+ return (
39
+ <div className="semiont-modal__actions semiont-modal__actions--wizard">
40
+ {onBack && backLabel && (
41
+ <button
42
+ type="button"
43
+ onClick={onBack}
44
+ className="semiont-button--secondary semiont-wizard-footer__back"
45
+ disabled={pending}
46
+ >
47
+ ◀ {backLabel}
48
+ </button>
49
+ )}
50
+ {primary && (
51
+ <button
52
+ type={primary.type}
53
+ {...(primary.onClick ? { onClick: primary.onClick } : {})}
54
+ className="semiont-button--primary"
55
+ disabled={pending}
56
+ data-pending={pending ? 'true' : 'false'}
57
+ >
58
+ {pending ? (primary.pendingLabel ?? primary.label) : primary.label}
59
+ </button>
60
+ )}
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,255 @@
1
+ /**
2
+ * INFERENCE-LIMITS-EXPOSURE P3b — the max-length control is bounded by the
3
+ * generation-serving agent's discovered output ceiling.
4
+ *
5
+ * The control was NOT previously unbounded: it shipped `min="100" max="4000"`.
6
+ * So this phase replaces a hardcoded 4000 with the real ceiling, which for any
7
+ * model above 4000 RAISES a cap users hit today, and for a smaller model
8
+ * lowers it to something the provider will actually honour.
9
+ */
10
+ import { describe, it, expect, vi } from 'vitest';
11
+ import { useState } from 'react';
12
+ import { render, screen, fireEvent } from '@testing-library/react';
13
+ import '@testing-library/jest-dom';
14
+ import type { CollaboratorEntry, GatheredContext } from '@semiont/core';
15
+ import { ConfigureGenerationStep } from '../ConfigureGenerationStep';
16
+
17
+ const translations = {
18
+ resourceTitle: 'Title',
19
+ resourceTitlePlaceholder: 'Title…',
20
+ additionalInstructions: 'Instructions',
21
+ additionalInstructionsPlaceholder: 'Instructions…',
22
+ language: 'Language',
23
+ languageHelp: 'Language help',
24
+ creativity: 'Creativity',
25
+ creativityFocused: 'Focused',
26
+ creativityCreative: 'Creative',
27
+ maxLength: 'Max length',
28
+ maxLengthHelp: 'How long the generated resource may be.',
29
+ // Named model + ceiling, so the bound is legible rather than mysterious (D6).
30
+ maxLengthCeiling: 'Limited to {{maxOutputTokens}} tokens by {{model}}.',
31
+ back: 'Back',
32
+ generate: 'Generate',
33
+ };
34
+
35
+ const context = { resources: [], annotations: [] } as unknown as GatheredContext;
36
+
37
+ /** The wizard's initial draft (WIZARD-NAVIGATION D3). */
38
+ const DRAFT = {
39
+ title: 'Untitled', storagePath: '', prompt: '', language: 'en',
40
+ temperature: 0.7, maxTokensText: '500',
41
+ };
42
+
43
+ const agentWithCeiling = (maxOutputTokens: number): CollaboratorEntry =>
44
+ ({
45
+ agent: {
46
+ '@type': 'Software',
47
+ name: 'Claude Sonnet 5',
48
+ provider: 'anthropic',
49
+ model: 'claude-sonnet-5',
50
+ },
51
+ servesJobTypes: ['generation'],
52
+ limits: { contextTokens: 200_000, maxOutputTokens },
53
+ }) as unknown as CollaboratorEntry;
54
+
55
+ /**
56
+ * The step is CONTROLLED since WIZARD-NAVIGATION D3 — the wizard owns the draft so
57
+ * Back cannot discard it. These tests therefore mount a tiny stateful harness rather
58
+ * than the bare component: a `vi.fn()` for `onConfigChange` would swallow every edit
59
+ * and quietly turn each assertion below into a test of nothing.
60
+ */
61
+ function Harness({ generationAgent }: { generationAgent?: CollaboratorEntry }) {
62
+ const [draft, setDraft] = useState(DRAFT);
63
+ return (
64
+ <ConfigureGenerationStep
65
+ config={draft}
66
+ onConfigChange={setDraft}
67
+ context={context}
68
+ onBack={vi.fn()}
69
+ onGenerate={onGenerateSpy}
70
+ translations={translations}
71
+ {...(generationAgent ? { generationAgent } : {})}
72
+ />
73
+ );
74
+ }
75
+
76
+ let onGenerateSpy = vi.fn();
77
+
78
+ function renderStep(generationAgent?: CollaboratorEntry) {
79
+ onGenerateSpy = vi.fn();
80
+ const utils = render(<Harness {...(generationAgent ? { generationAgent } : {})} />);
81
+ const input = () => screen.getByLabelText('Max length') as HTMLInputElement;
82
+ return { ...utils, input, props: { onGenerate: onGenerateSpy } };
83
+ }
84
+
85
+ function fillRequired() {
86
+ fireEvent.change(screen.getByLabelText('Save location'), {
87
+ target: { value: 'generated/out.md' },
88
+ });
89
+ }
90
+
91
+ describe('ConfigureGenerationStep — ceiling awareness', () => {
92
+ it('bounds the control by the agent ceiling and names it in help text', () => {
93
+ const { input } = renderStep(agentWithCeiling(64_000));
94
+
95
+ expect(input().max).toBe('64000');
96
+ expect(screen.getByText(/Limited to 64000 tokens by claude-sonnet-5\./)).toBeInTheDocument();
97
+ });
98
+
99
+ it('will not commit a value above the ceiling', () => {
100
+ // `max` on a number input does not prevent TYPING an over-value — it only
101
+ // marks the field invalid. D6 says the value cannot be entered, so assert
102
+ // the committed state, never the attribute alone.
103
+ const { input } = renderStep(agentWithCeiling(2_000));
104
+
105
+ fireEvent.change(input(), { target: { value: '9999' } });
106
+
107
+ expect(input().value).toBe('2000');
108
+ });
109
+
110
+ it('clamps the 500 default down when the ceiling is smaller', () => {
111
+ const { input } = renderStep(agentWithCeiling(300));
112
+
113
+ expect(input().value).toBe('300');
114
+ });
115
+
116
+ it('re-bounds and clamps when the ceiling arrives AFTER mount', () => {
117
+ // The case that decides whether this feature works in the app at all:
118
+ // `browse.agents()` is asynchronous, so `generationAgent` is undefined on
119
+ // first render and arrives later. An implementation that only consults the
120
+ // ceiling in the `useState` initializer passes every test above and does
121
+ // nothing in production.
122
+ const { input, rerender } = renderStep(undefined);
123
+
124
+ expect(input().max).toBe('4000');
125
+ fireEvent.change(input(), { target: { value: '3000' } });
126
+ expect(input().value).toBe('3000');
127
+
128
+ rerender(<Harness generationAgent={agentWithCeiling(1_000)} />);
129
+
130
+ expect(input().max).toBe('1000');
131
+ // 3000 is now out of range and must not survive as a submittable value.
132
+ expect(input().value).toBe('1000');
133
+ });
134
+
135
+ it('keeps a value that is still within a newly arrived ceiling', () => {
136
+ const { input, rerender } = renderStep(undefined);
137
+ fireEvent.change(input(), { target: { value: '800' } });
138
+
139
+ rerender(<Harness generationAgent={agentWithCeiling(4_000)} />);
140
+
141
+ expect(input().value).toBe('800');
142
+ });
143
+
144
+ it('falls back to today\'s bounds and copy without an agent (D3/D6 degradation)', () => {
145
+ const { input } = renderStep(undefined);
146
+
147
+ expect(input().min).toBe('100');
148
+ expect(input().max).toBe('4000');
149
+ expect(screen.getByText('How long the generated resource may be.')).toBeInTheDocument();
150
+ expect(screen.queryByText(/Limited to/)).not.toBeInTheDocument();
151
+ });
152
+
153
+ it('falls back when the agent is present but discovery could not answer', () => {
154
+ const noLimits = { ...agentWithCeiling(64_000) } as Record<string, unknown>;
155
+ delete noLimits.limits;
156
+ const { input } = renderStep(noLimits as unknown as CollaboratorEntry);
157
+
158
+ expect(input().max).toBe('4000');
159
+ expect(screen.queryByText(/Limited to/)).not.toBeInTheDocument();
160
+ });
161
+
162
+ it('submits the clamped value, not the typed one', () => {
163
+ const { props } = renderStep(agentWithCeiling(1_500));
164
+ const onGenerate = props.onGenerate;
165
+
166
+ fillRequired();
167
+ const input = screen.getByLabelText('Max length') as HTMLInputElement;
168
+ fireEvent.change(input, { target: { value: '8000' } });
169
+ fireEvent.click(screen.getByText(/Generate/));
170
+
171
+ expect(onGenerate).toHaveBeenCalledTimes(1);
172
+ expect(onGenerate.mock.calls[0]![0].maxTokens).toBe(1_500);
173
+ });
174
+
175
+ it('lets the field be cleared, and never submits NaN for it', () => {
176
+ // Pre-existing hazard the clamp must not inherit: the handler was
177
+ // `parseInt(e.target.value)` and `parseInt('')` is NaN, which React then
178
+ // warns about and which would travel into the job config. Clearing must
179
+ // stay possible (you cannot retype a number otherwise), so the contract is
180
+ // about what SUBMITS, not about forbidding the empty state.
181
+ const { props } = renderStep(agentWithCeiling(4_000));
182
+ const onGenerate = props.onGenerate;
183
+ const input = screen.getByLabelText('Max length') as HTMLInputElement;
184
+
185
+ fillRequired();
186
+ fireEvent.change(input, { target: { value: '' } });
187
+ expect(input.value).toBe('');
188
+
189
+ fireEvent.click(screen.getByText(/Generate/));
190
+
191
+ const submitted = onGenerate.mock.calls[0]![0].maxTokens;
192
+ expect(Number.isFinite(submitted)).toBe(true);
193
+ expect(submitted).toBeGreaterThanOrEqual(100);
194
+ expect(submitted).toBeLessThanOrEqual(4_000);
195
+ });
196
+ });
197
+
198
+ // The rest of the draft, same contract as the ceiling: every field reports to
199
+ // the owner (WIZARD-NAVIGATION D3). A field that kept its own state would look
200
+ // right until someone pressed Back.
201
+ describe('ConfigureGenerationStep — the draft is the owner\'s', () => {
202
+ it('reports edits to title, instructions, language and creativity', async () => {
203
+ const { input } = renderStep(agentWithCeiling(4_000));
204
+ expect(input()).toBeInTheDocument();
205
+
206
+ fireEvent.change(screen.getByLabelText(translations.resourceTitle), { target: { value: 'Caspian Sea' } });
207
+ expect((screen.getByLabelText(translations.resourceTitle) as HTMLInputElement).value).toBe('Caspian Sea');
208
+
209
+ fireEvent.change(screen.getByLabelText(translations.additionalInstructions), { target: { value: 'hydrology only' } });
210
+ expect((screen.getByLabelText(translations.additionalInstructions) as HTMLTextAreaElement).value).toBe('hydrology only');
211
+
212
+ // The label carries the live value — "Creativity (0.7)" — which is itself
213
+ // the proof the slider is controlled: it can only update if the owner's
214
+ // draft came back down.
215
+ const slider = () => screen.getByLabelText(new RegExp(`^${translations.creativity}`)) as HTMLInputElement;
216
+ fireEvent.change(slider(), { target: { value: '0.2' } });
217
+ expect(slider().value).toBe('0.2');
218
+ expect(screen.getByText(/Creativity \(0\.2\)/)).toBeInTheDocument();
219
+ });
220
+
221
+ it('lets the max-length field go empty while retyping', () => {
222
+ // Clearing is a legitimate mid-edit state — you cannot retype a number
223
+ // otherwise — so the field must accept '' and let submission clamp it.
224
+ const { input } = renderStep(agentWithCeiling(4_000));
225
+ fireEvent.change(input(), { target: { value: '' } });
226
+ expect(input().value).toBe('');
227
+ });
228
+
229
+ it('reports a language change to the owner', () => {
230
+ renderStep(agentWithCeiling(4_000));
231
+ const select = () => screen.getByLabelText(translations.language) as HTMLSelectElement;
232
+ const other = Array.from(select().options).map((o) => o.value).find((v) => v !== select().value);
233
+ fireEvent.change(select(), { target: { value: other } });
234
+ expect(select().value).toBe(other);
235
+ });
236
+
237
+ it('carries the typed instructions into the submitted config, trimmed', () => {
238
+ const { props } = renderStep(agentWithCeiling(4_000));
239
+ fillRequired();
240
+ fireEvent.change(screen.getByLabelText(translations.additionalInstructions), { target: { value: ' hydrology only ' } });
241
+ fireEvent.click(screen.getByText(new RegExp(translations.generate)));
242
+
243
+ expect(props.onGenerate).toHaveBeenCalledTimes(1);
244
+ expect(props.onGenerate.mock.calls[0]![0].prompt).toBe('hydrology only');
245
+ });
246
+
247
+ it('omits the prompt entirely when the field is blank', () => {
248
+ // `prompt` is optional on the wire; sending "" would be a claim the user
249
+ // never made, and the worker would prepend an empty instruction block.
250
+ const { props } = renderStep(agentWithCeiling(4_000));
251
+ fillRequired();
252
+ fireEvent.click(screen.getByText(new RegExp(translations.generate)));
253
+ expect(props.onGenerate.mock.calls[0]![0]).not.toHaveProperty('prompt');
254
+ });
255
+ });
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The search step is CONTROLLED (WIZARD-NAVIGATION D3) — the wizard owns
3
+ * `limit` and `useSemanticScoring` so stepping Back cannot discard them.
4
+ *
5
+ * That makes these tests about the contract rather than the widget: every edit
6
+ * must reach `onConfigChange` (an uncontrolled field would appear to work and
7
+ * silently revert on Back), and submit must send the CURRENT config rather than
8
+ * a stale copy.
9
+ */
10
+ import { describe, it, expect, vi } from 'vitest';
11
+ import { render, screen } from '@testing-library/react';
12
+ import userEvent from '@testing-library/user-event';
13
+ import '@testing-library/jest-dom';
14
+ import { ConfigureSearchStep, type SearchConfig } from '../ConfigureSearchStep';
15
+
16
+ const T = {
17
+ maxResults: 'Max Results', semanticScoring: 'Semantic Scoring',
18
+ semanticScoringHelp: 'Use AI to score results by semantic relevance',
19
+ back: 'Back', search: 'Search', searching: 'Searching…',
20
+ };
21
+
22
+ const CONFIG: SearchConfig = { limit: 10, useSemanticScoring: true };
23
+
24
+ function renderStep(config: SearchConfig = CONFIG) {
25
+ const onConfigChange = vi.fn();
26
+ const onSearch = vi.fn();
27
+ const onBack = vi.fn();
28
+ render(
29
+ <ConfigureSearchStep
30
+ config={config}
31
+ onConfigChange={onConfigChange}
32
+ onBack={onBack}
33
+ onSearch={onSearch}
34
+ translations={T}
35
+ />,
36
+ );
37
+ return { onConfigChange, onSearch, onBack };
38
+ }
39
+
40
+ describe('ConfigureSearchStep', () => {
41
+ it('reports a new result limit to the owner rather than keeping it', async () => {
42
+ const { onConfigChange } = renderStep();
43
+ await userEvent.selectOptions(screen.getByLabelText(T.maxResults), '20');
44
+ expect(onConfigChange).toHaveBeenCalledWith({ limit: 20, useSemanticScoring: true });
45
+ });
46
+
47
+ it('reports the semantic-scoring toggle the same way', async () => {
48
+ const { onConfigChange } = renderStep();
49
+ await userEvent.click(screen.getByLabelText(new RegExp(T.semanticScoring)));
50
+ expect(onConfigChange).toHaveBeenCalledWith({ limit: 10, useSemanticScoring: false });
51
+ });
52
+
53
+ it('submits whatever the owner currently holds, not a default', async () => {
54
+ // The point of controlled state: a step that kept its own copy could submit
55
+ // something the wizard never saw.
56
+ const { onSearch } = renderStep({ limit: 5, useSemanticScoring: false });
57
+ await userEvent.click(screen.getByRole('button', { name: T.search }));
58
+ expect(onSearch).toHaveBeenCalledWith({ limit: 5, useSemanticScoring: false });
59
+ });
60
+
61
+ it('renders the owner\'s values, so Back-then-forward shows them again', () => {
62
+ renderStep({ limit: 20, useSemanticScoring: false });
63
+ expect((screen.getByLabelText(T.maxResults) as HTMLSelectElement).value).toBe('20');
64
+ expect((screen.getByLabelText(new RegExp(T.semanticScoring)) as HTMLInputElement).checked).toBe(false);
65
+ });
66
+
67
+ it('retreats when Back is pressed', async () => {
68
+ const { onBack } = renderStep();
69
+ await userEvent.click(screen.getByRole('button', { name: /Back/ }));
70
+ expect(onBack).toHaveBeenCalledTimes(1);
71
+ });
72
+ });