@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
@@ -2,8 +2,9 @@
2
2
 
3
3
  import React, { useEffect } from 'react';
4
4
  import { WizardFooter } from './WizardFooter';
5
- import type { CollaboratorEntry, GatheredContext } from '@semiont/core';
6
- import { LOCALES } from '@semiont/core';
5
+ import type { CollaboratorEntry, GatheredContext, SupportedMediaType } from '@semiont/core';
6
+ import type { GenerationOptions } from '@semiont/sdk';
7
+ import { LOCALES, GENERATABLE_MEDIA_TYPES, capabilitiesOf, proposeStoragePath } from '@semiont/core';
7
8
 
8
9
  /**
9
10
  * Bounds for the max-length control when no ceiling is known. These are the
@@ -15,34 +16,103 @@ const DEFAULT_MAX_TOKENS_CEILING = 4000;
15
16
  /** The step's form values, owned by the wizard so Back cannot discard them (D3). */
16
17
  export interface GenerationDraft {
17
18
  title: string;
19
+ /**
20
+ * The path FRAGMENT the user types beside the `file://` prefix — not a URI.
21
+ * (`GenerationConfig.storageUri` is the assembled one; the names differ
22
+ * because the things differ.)
23
+ */
18
24
  storagePath: string;
19
25
  prompt: string;
20
26
  language: string;
21
27
  temperature: number;
22
28
  /** Text, not number: the field is user-editable and may be mid-edit or empty. */
23
29
  maxTokensText: string;
30
+ /** The artifact's media type. Seeded `text/markdown` by both hosts (D2). */
31
+ outputMediaType: SupportedMediaType;
24
32
  }
25
33
 
26
- export interface GenerationConfig {
27
- title: string;
28
- storagePath: string;
29
- prompt?: string;
30
- language: string;
31
- temperature: number;
32
- maxTokens: number;
33
- context: GatheredContext;
34
- }
34
+ /**
35
+ * What this form fills in. Named here, but TYPED by the SDK (P2b).
36
+ *
37
+ * `storageUri` is on this list rather than a local `storagePath: string`
38
+ * because the two were the same thing under different names — a divergence
39
+ * that survived precisely because nothing tied them together (D8).
40
+ */
41
+ type FormFilled =
42
+ | 'title'
43
+ | 'storageUri'
44
+ | 'language'
45
+ | 'temperature'
46
+ | 'maxTokens'
47
+ | 'outputMediaType';
48
+
49
+ /**
50
+ * The submitted payload: the SDK's own generation options, DERIVED rather
51
+ * than restated (P2b), plus the grounding context.
52
+ *
53
+ * This was a hand-written interface listing the same fields with independent
54
+ * types — the shape `## Duplicated Shape` forbids — and it drifted exactly as
55
+ * predicted: `storagePath: string` held a `storageUri`, so every consumer
56
+ * re-mapped it by hand and silently dropped whatever the mapping forgot.
57
+ * Deriving means a rename or retype in `GenerationOptions` is a compile
58
+ * error here instead of a quiet gap at the wire.
59
+ *
60
+ * NARROWED on purpose: the SDK also carries `task`, `structure`, `cite` and
61
+ * `stallDeadlineMs`, which this form does not collect, and a payload type
62
+ * that advertised them would promise more than the UI delivers. Surfacing one
63
+ * later is a one-word edit to `FormFilled` plus its control — the door is
64
+ * unlocked, not open.
65
+ *
66
+ * `Required` because the form always fills these, even where the SDK lets
67
+ * them be absent; `prompt` stays optional, because a blank instruction field
68
+ * is omitted rather than sent empty.
69
+ */
70
+ export type GenerationConfig =
71
+ & Required<Pick<GenerationOptions, FormFilled>>
72
+ & Pick<GenerationOptions, 'prompt'>
73
+ & { context: GatheredContext };
74
+
75
+ /** The one place a format's required extension is read (D7). */
76
+ const extensionFor = (format: SupportedMediaType): string =>
77
+ capabilitiesOf(format)?.extension ?? '';
78
+
79
+ /**
80
+ * A fresh draft, defined ONCE for both hosts (D8b). The two modals carried
81
+ * byte-identical literals — magic numbers included — so every new field meant
82
+ * two edits and an opportunity to seed only one of them.
83
+ */
84
+ export const freshGenerationDraft = (title: string, locale: string): GenerationDraft => ({
85
+ title,
86
+ storagePath: '',
87
+ prompt: '',
88
+ language: locale,
89
+ temperature: 0.7,
90
+ maxTokensText: '500',
91
+ // D2: markdown is the DEFAULT, not an assumption — the worker would also
92
+ // default to it, but a control must send what it shows.
93
+ outputMediaType: 'text/markdown',
94
+ });
35
95
 
36
96
  export interface ConfigureGenerationStepProps {
37
97
  context: GatheredContext;
98
+ /** Echo of the gather step's hint — the thing being steered stays visible (GEP D8). */
99
+ hintEcho?: { label: string; value: string };
38
100
  /** Owned by the wizard so Back is lossless (WIZARD-NAVIGATION D3). */
39
101
  config: GenerationDraft;
40
102
  onConfigChange: (config: GenerationDraft) => void;
41
- onBack: () => void;
103
+ /** Absent in a single-stack host (GATHER-AT-THE-TOP D6) the footer then renders no retreat. */
104
+ onBack?: () => void;
42
105
  onGenerate: (config: GenerationConfig) => void;
106
+ /**
107
+ * Folder of the resource being generated FROM, so the artifact lands beside
108
+ * its source (D11). The page derives it; this component stays
109
+ * presentational, exactly as it already receives the default title.
110
+ */
111
+ defaultFolder?: string;
43
112
  translations: {
44
113
  resourceTitle: string;
45
114
  resourceTitlePlaceholder: string;
115
+ saveLocation: string;
46
116
  additionalInstructions: string;
47
117
  additionalInstructionsPlaceholder: string;
48
118
  language: string;
@@ -58,7 +128,16 @@ export interface ConfigureGenerationStepProps {
58
128
  * and `{{model}}`.
59
129
  */
60
130
  maxLengthCeiling: string;
61
- back: string;
131
+ /** Label for the output-format select. */
132
+ outputFormat: string;
133
+ /**
134
+ * Shown when the Save location's extension contradicts the chosen format,
135
+ * which the form REFUSES to submit (D7 — the worker is faithful and will
136
+ * write a PDF to a `.md` path, so this is the only gate). Interpolates
137
+ * `{{extension}}`.
138
+ */
139
+ formatExtensionMismatch: string;
140
+ back?: string;
62
141
  generate: string;
63
142
  };
64
143
  /**
@@ -73,10 +152,12 @@ export interface ConfigureGenerationStepProps {
73
152
 
74
153
  export function ConfigureGenerationStep({
75
154
  context,
155
+ hintEcho,
76
156
  config,
77
157
  onConfigChange,
78
158
  onBack,
79
159
  onGenerate,
160
+ defaultFolder = '',
80
161
  translations: t,
81
162
  generationAgent,
82
163
  }: ConfigureGenerationStepProps) {
@@ -85,9 +166,28 @@ export function ConfigureGenerationStep({
85
166
  // both sliders. `maxTokensText` stays TEXT rather than a number so the field can
86
167
  // be cleared mid-edit — `parseInt` on an empty field yields NaN, which React
87
168
  // warns about and which used to travel into the job config.
88
- const { title, storagePath, prompt, language, temperature, maxTokensText } = config;
169
+ const { title, storagePath, prompt, language, temperature, maxTokensText, outputMediaType } = config;
89
170
  const set = (patch: Partial<GenerationDraft>) => onConfigChange({ ...config, ...patch });
90
171
 
172
+ // D11 — the Save location starts filled and FOLLOWS the title and format
173
+ // until the user takes it over. `pathTouched` is DERIVED rather than stored:
174
+ // it is exactly "the field holds the user's own text", which makes clearing
175
+ // the field un-touch it for free, keeps it reset-on-open and Back-safe
176
+ // because `storagePath` already is, and leaves no second field to desync.
177
+ const pathTouched = storagePath !== '';
178
+ const proposedPath = proposeStoragePath(defaultFolder, title, outputMediaType);
179
+ const effectivePath = pathTouched ? storagePath : proposedPath;
180
+
181
+ // D7 — the GUI is where a format/extension mismatch is caught, because it is
182
+ // where the person who can fix it is standing. An EMPTY path is not a
183
+ // mismatch: the field is `required`, so emptiness is already refused, and
184
+ // telling an untouched form it is wrong is not a welcome. Under D11 a
185
+ // PROPOSED path always matches, so this now fires only on hand-edits.
186
+ const requiredExtension = extensionFor(outputMediaType);
187
+ const extensionMismatch =
188
+ effectivePath !== '' &&
189
+ !effectivePath.toLowerCase().endsWith(requiredExtension.toLowerCase());
190
+
91
191
  const ceiling = generationAgent?.limits?.maxOutputTokens ?? DEFAULT_MAX_TOKENS_CEILING;
92
192
 
93
193
  /**
@@ -114,20 +214,30 @@ export function ConfigureGenerationStep({
114
214
 
115
215
  const handleSubmit = (e: React.FormEvent) => {
116
216
  e.preventDefault();
217
+ if (extensionMismatch) return;
117
218
  const trimmedPrompt = prompt.trim();
118
219
  onGenerate({
119
220
  title,
120
- storagePath: `file://${storagePath}`,
221
+ storageUri: `file://${effectivePath}`,
121
222
  ...(trimmedPrompt ? { prompt: trimmedPrompt } : {}),
122
223
  language,
123
224
  temperature,
124
225
  maxTokens: submittedMaxTokens,
226
+ outputMediaType,
125
227
  context,
126
228
  });
127
229
  };
128
230
 
129
231
  return (
130
- <form onSubmit={handleSubmit} className="semiont-form semiont-form--scrollable">
232
+ // No --scrollable: this form renders below the evidence display inside the
233
+ // host's single step-scroll pane — an independent scroll region here is
234
+ // what squeezed the parameters out of view (the measured failure).
235
+ <form onSubmit={handleSubmit} className="semiont-form">
236
+ {hintEcho && (
237
+ <p className="semiont-wizard__hint-echo">
238
+ {hintEcho.label}: {hintEcho.value}
239
+ </p>
240
+ )}
131
241
  {/* Resource Title */}
132
242
  <div className="semiont-form__field">
133
243
  <label htmlFor="wizard-title" className="semiont-form__label">
@@ -147,20 +257,51 @@ export function ConfigureGenerationStep({
147
257
  {/* Storage URI */}
148
258
  <div className="semiont-form__field">
149
259
  <label htmlFor="wizard-storagePath" className="semiont-form__label">
150
- Save location
260
+ {t.saveLocation}
151
261
  </label>
152
262
  <div className="semiont-input-addon">
153
263
  <span className="semiont-input-addon__prefix">file://</span>
154
264
  <input
155
265
  id="wizard-storagePath"
156
266
  type="text"
157
- value={storagePath}
267
+ value={effectivePath}
158
268
  onChange={(e) => set({ storagePath: e.target.value })}
159
269
  required
270
+ aria-invalid={extensionMismatch}
271
+ {...(extensionMismatch ? { 'aria-describedby': 'wizard-format-mismatch' } : {})}
160
272
  className="semiont-input semiont-input--addon"
161
273
  placeholder="generated/my-resource.md"
162
274
  />
163
275
  </div>
276
+ {/* The reason sits with the field, not on the button: the message
277
+ belongs where the fix is. */}
278
+ {extensionMismatch && (
279
+ <p id="wizard-format-mismatch" className="semiont-form__error" role="alert">
280
+ {t.formatExtensionMismatch.replace('{{extension}}', requiredExtension)}
281
+ </p>
282
+ )}
283
+ </div>
284
+
285
+ {/* Output format — with the artifact's IDENTITY (title, location), not
286
+ among the model knobs below (D5). The options are DERIVED from the
287
+ registry (D1): promote a fourth row to `generatable` and it appears
288
+ here, with no list to forget to update. */}
289
+ <div className="semiont-form__field">
290
+ <label htmlFor="wizard-outputFormat" className="semiont-form__label">
291
+ {t.outputFormat}
292
+ </label>
293
+ <select
294
+ id="wizard-outputFormat"
295
+ value={outputMediaType}
296
+ onChange={(e) => set({ outputMediaType: e.target.value as SupportedMediaType })}
297
+ className="semiont-select"
298
+ >
299
+ {GENERATABLE_MEDIA_TYPES.map((format) => (
300
+ <option key={format} value={format}>
301
+ {capabilitiesOf(format)?.label ?? format}
302
+ </option>
303
+ ))}
304
+ </select>
164
305
  </div>
165
306
 
166
307
  {/* Additional Instructions */}
@@ -198,7 +339,7 @@ export function ConfigureGenerationStep({
198
339
  </select>
199
340
  </div>
200
341
 
201
- <div className="semiont-form__field semiont-form__field--inline semiont-form__field--grow">
342
+ <div className="semiont-form__field semiont-form__field--inline semiont-form__field--slider">
202
343
  <label htmlFor="wizard-temperature" className="semiont-form__label">
203
344
  {t.creativity} ({temperature.toFixed(1)})
204
345
  </label>
@@ -218,7 +359,7 @@ export function ConfigureGenerationStep({
218
359
  </div>
219
360
  </div>
220
361
 
221
- <div className="semiont-form__field semiont-form__field--inline semiont-form__field--narrow">
362
+ <div className="semiont-form__field semiont-form__field--inline semiont-form__field--narrow semiont-form__field--end">
222
363
  <label htmlFor="wizard-maxTokens" className="semiont-form__label">
223
364
  {t.maxLength}
224
365
  </label>
@@ -242,24 +383,27 @@ export function ConfigureGenerationStep({
242
383
  }}
243
384
  className="semiont-input"
244
385
  />
245
- <p className="semiont-form__help">
246
- {generationAgent?.limits
247
- ? t.maxLengthCeiling
248
- .replace('{{maxOutputTokens}}', String(ceiling))
249
- .replace(
250
- '{{model}}',
251
- (generationAgent.agent as { model?: string; name?: string }).model
252
- ?? generationAgent.agent.name,
253
- )
254
- : t.maxLengthHelp}
255
- </p>
256
386
  </div>
257
387
  </div>
258
388
 
389
+ {/* The bound, as a full sentence — which is why it lives UNDER the row,
390
+ never inside the 5.5rem Max Length column: a sentence in that box
391
+ wraps one word per line and its height taxes the whole flex row. */}
392
+ <p className="semiont-form__help semiont-form__help--row">
393
+ {generationAgent?.limits
394
+ ? t.maxLengthCeiling
395
+ .replace('{{maxOutputTokens}}', String(ceiling))
396
+ .replace(
397
+ '{{model}}',
398
+ (generationAgent.agent as { model?: string; name?: string }).model
399
+ ?? generationAgent.agent.name,
400
+ )
401
+ : t.maxLengthHelp}
402
+ </p>
403
+
259
404
  <WizardFooter
260
- backLabel={t.back}
261
- onBack={onBack}
262
- primary={{ label: t.generate, type: 'submit' }}
405
+ {...(onBack && t.back ? { backLabel: t.back, onBack } : {})}
406
+ primary={{ label: t.generate, type: 'submit', disabled: extensionMismatch }}
263
407
  />
264
408
  </form>
265
409
  );
@@ -16,8 +16,13 @@ export interface ConfigureSearchStepProps {
16
16
  * invites the press. The wizard owns it for the modal's lifetime instead.
17
17
  */
18
18
  config: SearchConfig;
19
+ /** Echo of the gather step's hint — the thing being steered stays visible (GEP D8). */
20
+ hintEcho?: { label: string; value: string };
19
21
  onConfigChange: (config: SearchConfig) => void;
20
22
  isSearching?: boolean;
23
+ /** A settled failure (emit refused, matcher error, timeout) — the spinner
24
+ * must never outlive the request that fed it. */
25
+ searchError?: string | null;
21
26
  onBack: () => void;
22
27
  onSearch: (config: SearchConfig) => void;
23
28
  translations: {
@@ -27,13 +32,16 @@ export interface ConfigureSearchStepProps {
27
32
  back: string;
28
33
  search: string;
29
34
  searching: string;
35
+ searchFailed: string;
30
36
  };
31
37
  }
32
38
 
33
39
  export function ConfigureSearchStep({
34
40
  config,
41
+ hintEcho,
35
42
  onConfigChange,
36
43
  isSearching = false,
44
+ searchError = null,
37
45
  onBack,
38
46
  onSearch,
39
47
  translations: t,
@@ -45,6 +53,11 @@ export function ConfigureSearchStep({
45
53
 
46
54
  return (
47
55
  <form onSubmit={handleSubmit} className="semiont-form">
56
+ {hintEcho && (
57
+ <p className="semiont-wizard__hint-echo">
58
+ {hintEcho.label}: {hintEcho.value}
59
+ </p>
60
+ )}
48
61
  {/* Max Results */}
49
62
  <div className="semiont-form__field">
50
63
  <label htmlFor="wizard-limit" className="semiont-form__label">
@@ -78,6 +91,12 @@ export function ConfigureSearchStep({
78
91
  </p>
79
92
  </div>
80
93
 
94
+ {searchError && (
95
+ <div role="alert" style={{ textAlign: 'center', padding: '0.5rem 0', color: 'var(--semiont-color-red-600)' }}>
96
+ {t.searchFailed}: {searchError}
97
+ </div>
98
+ )}
99
+
81
100
  <WizardFooter
82
101
  backLabel={t.back}
83
102
  onBack={onBack}
@@ -1,13 +1,17 @@
1
1
  'use client';
2
2
 
3
- import { deriveViews, type GatheredContext } from '@semiont/core';
3
+ import { deriveViews, getBodySource, getExactText, getTargetSelector, type GatheredContext } from '@semiont/core';
4
4
 
5
5
  export interface ContextSummaryTranslations {
6
6
  sourceContextLabel: string;
7
7
  connectionsLabel: string;
8
8
  citedByLabel: string;
9
- userHintLabel: string;
10
- userHintPlaceholder: string;
9
+ /** GEP P2: the graph pane's chrome travels with this component (D10). */
10
+ graphPaneTitle: string;
11
+ /** Strategy-relevant empty state — emptiness is evidence (GEP D1). */
12
+ graphEmpty: string;
13
+ /** Resource-level linking annotation whose target the graph can't name. */
14
+ resourceLinkLabel: string;
11
15
  }
12
16
 
13
17
  export interface ContextSummaryProps {
@@ -18,85 +22,194 @@ export interface ContextSummaryProps {
18
22
  export function ContextSummary({ context, translations: t }: ContextSummaryProps) {
19
23
  // Graph views are focus-agnostic — only the focal resource id (and an optional
20
24
  // focal annotation id) differ by kind.
25
+ // ResourceDescriptor's identifier is `@id` (JSON-LD) — its open index
26
+ // signature makes a bare `.id` deref compile and silently yield undefined.
21
27
  const mainResourceId = context.focus.kind === 'annotation'
22
- ? String(context.focus.sourceResource.id)
23
- : String(context.focus.resource.id);
28
+ ? context.focus.sourceResource['@id']
29
+ : context.focus.resource['@id'];
24
30
  const focalAnnotationId = context.focus.kind === 'annotation'
25
31
  ? context.focus.annotation.id
26
32
  : undefined;
27
- const { connections, citedBy, citedByCount } = deriveViews(
33
+ const { connections, citedBy } = deriveViews(
28
34
  context.graph,
29
35
  mainResourceId,
30
36
  focalAnnotationId,
31
37
  );
32
38
 
39
+ // Sibling NODES — the same structural annotation-of walk core uses for its
40
+ // entity-type set, collected at node granularity because the viz draws them
41
+ // (GEP P4; core's `siblingEntityTypes` stays the prompt-side view).
42
+ const annotationOf = new Map<string, string>();
43
+ for (const edge of context.graph.edges) {
44
+ if (edge.type === 'annotation-of') annotationOf.set(edge.source, edge.target);
45
+ }
46
+ const siblings = context.graph.nodes.filter(
47
+ (n) => n.type === 'annotation' && n.id !== focalAnnotationId && annotationOf.get(n.id) === mainResourceId,
48
+ );
49
+
50
+ // Rarity-ranked entity types for hover-text — the IDF semantics
51
+ // `entityTypeFrequencies` was stored for (D3).
52
+ const freq: Record<string, number> =
53
+ (context.metadata?.entityTypeFrequencies as Record<string, number> | undefined) ?? {};
54
+ const rank = (types: string[]) => [...types].sort((a, b) => (freq[a] ?? 0) - (freq[b] ?? 0));
55
+
56
+ const firstBodyValue = (body: unknown): string | undefined => {
57
+ // Tagging-purpose bodies are the entity-type tag — already shown via
58
+ // entityTypes, so skip them rather than repeat the type in the hover.
59
+ for (const item of Array.isArray(body) ? body : [body]) {
60
+ if (item && typeof item === 'object' && 'value' in item
61
+ && (item as { purpose?: string }).purpose !== 'tagging') {
62
+ return (item as { value?: string }).value;
63
+ }
64
+ }
65
+ return undefined;
66
+ };
67
+
68
+ // ── Deterministic layered layout (D3): citers | focal + siblings | peers ──
69
+ // Positions derive from array order alone — no physics, no randomness.
70
+ // Geometry adapts to what's populated (no-clipping principle): an absent
71
+ // citer/peer layer frees its column for the sibling band to wrap into, and
72
+ // the viewBox shrinks to the columns actually used. Everything always draws
73
+ // — overflow rides the wizard pane's scroll, never a cap.
74
+ const NODE_W = 180;
75
+ const NODE_H = 34;
76
+ const ROW = 52;
77
+ const TOP = 8;
78
+ const COL_PITCH = 216;
79
+ const colX = (col: number) => 8 + col * COL_PITCH;
80
+
81
+ type DrawnNode = {
82
+ id: string; label: string; kind: 'focal' | 'resource' | 'annotation';
83
+ hover: string; x: number; y: number;
84
+ };
85
+ type DrawnEdge = { key: string; kind: 'cites' | 'peer' | 'annotation-of'; hover: string;
86
+ x1: number; y1: number; x2: number; y2: number };
87
+
88
+ const focalNode = context.graph.nodes.find((n) => n.id === mainResourceId);
89
+ const focalLabel = focalNode?.label
90
+ ?? (context.focus.kind === 'annotation' ? context.focus.sourceResource.name : context.focus.resource.name)
91
+ ?? mainResourceId;
92
+
93
+ const drawn: DrawnNode[] = [];
94
+ const drawnEdges: DrawnEdge[] = [];
95
+
96
+ // The sibling band wraps across the columns absent layers free (3 total).
97
+ const hasCiters = citedBy.length > 0;
98
+ const hasPeers = connections.length > 0;
99
+ const bandCols = Math.max(1, Math.min(
100
+ 3 - (hasCiters ? 1 : 0) - (hasPeers ? 1 : 0),
101
+ siblings.length,
102
+ ));
103
+ const bandCol0 = hasCiters ? 1 : 0;
104
+ const numCols = (hasCiters ? 1 : 0) + bandCols + (hasPeers ? 1 : 0);
105
+
106
+ const focal: DrawnNode = { id: mainResourceId, label: focalLabel, kind: 'focal', hover: focalLabel, x: colX(bandCol0), y: TOP };
107
+ drawn.push(focal);
108
+
109
+ citedBy.forEach((c, i) => {
110
+ const node: DrawnNode = { id: c.resourceId, label: c.resourceName, kind: 'resource',
111
+ hover: `${c.resourceName}\n${t.citedByLabel}`, x: colX(0), y: TOP + i * ROW };
112
+ drawn.push(node);
113
+ drawnEdges.push({ key: `cites-${c.resourceId}`, kind: 'cites', hover: 'cites',
114
+ x1: node.x + NODE_W, y1: node.y + NODE_H / 2, x2: focal.x, y2: focal.y + NODE_H / 2 });
115
+ });
116
+
117
+ connections.forEach((c, i) => {
118
+ const types = rank(c.entityTypes);
119
+ const node: DrawnNode = { id: c.resourceId, label: c.resourceName, kind: 'resource',
120
+ hover: types.length ? `${c.resourceName}\n${types.join(', ')}` : c.resourceName,
121
+ x: colX(bandCol0 + bandCols), y: TOP + i * ROW };
122
+ drawn.push(node);
123
+ // With a multi-column band the line crosses the band's row 0, which only
124
+ // ever holds the focal node in the band's FIRST column — so it stays clear.
125
+ drawnEdges.push({ key: `peer-${c.resourceId}`, kind: 'peer',
126
+ hover: c.bidirectional ? `${t.connectionsLabel} · mutual` : t.connectionsLabel,
127
+ x1: focal.x + NODE_W, y1: focal.y + NODE_H / 2, x2: node.x, y2: node.y + NODE_H / 2 });
128
+ });
129
+
130
+ siblings.forEach((s, i) => {
131
+ // Discriminated: type === 'annotation' guarantees the embedded W3C
132
+ // annotation (P3/D11). The node's identity is the text it wraps — a
133
+ // column of motivation labels is uninterpretable; motivation shows as
134
+ // the node's styling and hover instead.
135
+ const ann = s.type === 'annotation' ? s.annotation : undefined;
136
+ const quote = ann ? getExactText(getTargetSelector(ann.target)) : '';
137
+ const motivation = ann?.motivation ?? s.label;
138
+ const body = ann ? firstBodyValue(ann.body) : undefined;
139
+ const types = rank(s.entityTypes ?? []);
140
+ // A resource-level annotation has no selector — nothing to quote — but is
141
+ // ABOUT a target resource: label it by what it links to. Raw motivation
142
+ // ("linking") is internal vocabulary and stays hover-only.
143
+ const linkTarget = !quote && ann ? getBodySource(ann.body) : null;
144
+ const linkedName = linkTarget
145
+ ? context.graph.nodes.find((n) => n.id === linkTarget)?.label
146
+ : undefined;
147
+ const label = quote
148
+ || (linkedName ? `→ ${linkedName}` : undefined)
149
+ || (linkTarget ? t.resourceLinkLabel : undefined)
150
+ || body
151
+ || s.label;
152
+ // Hover leads with the full label — the drawn one truncates (like the
153
+ // resource nodes, whose hover carries the full name). Deduped: a label
154
+ // that fell through to the body or motivation must not repeat below it.
155
+ const hover = Array.from(new Set(
156
+ [label, motivation, body, types.length ? types.join(', ') : undefined].filter(Boolean),
157
+ )).join('\n');
158
+ const node: DrawnNode = { id: s.id, label, kind: 'annotation',
159
+ hover, x: colX(bandCol0 + (i % bandCols)), y: TOP + (1 + Math.floor(i / bandCols)) * ROW };
160
+ drawn.push(node);
161
+ drawnEdges.push({ key: `annof-${s.id}`, kind: 'annotation-of', hover: 'annotation-of',
162
+ x1: focal.x + NODE_W / 2, y1: focal.y + NODE_H, x2: node.x + NODE_W / 2, y2: node.y });
163
+ });
164
+
165
+ const rows = Math.max(citedBy.length, 1 + Math.ceil(siblings.length / bandCols), connections.length);
166
+ const width = numCols * COL_PITCH - 20;
167
+ const height = TOP + rows * ROW + 8;
168
+ const truncate = (s: string) => (s.length > 22 ? `${s.slice(0, 21)}…` : s);
169
+ const hasBody = connections.length > 0 || citedBy.length > 0 || siblings.length > 0;
170
+
171
+ // GEP P4: this component IS the graph pane, and its body IS a graph — a
172
+ // hand-rolled deterministic SVG (D3: no physics, no library, no pan-zoom;
173
+ // a decision surface, not an explorer). Chrome travels with the component,
174
+ // so every consumer inherits it (D10). Emptiness is evidence (D1).
33
175
  return (
34
- <>
35
- {(connections.length > 0 || citedByCount > 0) && (
36
- <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
37
-
38
- {connections.length > 0 && (
39
- <div>
40
- <div style={{
41
- fontSize: 'var(--semiont-text-xs)',
42
- fontWeight: 700,
43
- textTransform: 'uppercase',
44
- letterSpacing: '0.06em',
45
- color: 'var(--semiont-text-tertiary)',
46
- marginBottom: '0.375rem',
47
- }}>
48
- {t.connectionsLabel}
49
- </div>
50
- <ul style={{ display: 'flex', flexDirection: 'column', gap: '0.375rem', listStyle: 'none', padding: 0, margin: 0 }}>
51
- {connections.map(conn => (
52
- <li key={conn.resourceId} style={{
53
- display: 'flex',
54
- flexWrap: 'wrap',
55
- alignItems: 'center',
56
- gap: '0.375rem',
57
- fontSize: 'var(--semiont-text-sm)',
58
- }}>
59
- <span style={{ color: 'var(--semiont-text-primary)', fontWeight: 500 }}>{conn.resourceName}</span>
60
- {conn.bidirectional && (
61
- <span className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.375rem' }}>mutual</span>
62
- )}
63
- {conn.entityTypes && conn.entityTypes.map(et => (
64
- <span key={et} className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.375rem' }}>
65
- {et}
66
- </span>
67
- ))}
68
- </li>
69
- ))}
70
- </ul>
71
- </div>
72
- )}
73
-
74
- {citedByCount > 0 && (
75
- <div>
76
- <div style={{
77
- fontSize: 'var(--semiont-text-xs)',
78
- fontWeight: 700,
79
- textTransform: 'uppercase',
80
- letterSpacing: '0.06em',
81
- color: 'var(--semiont-text-tertiary)',
82
- marginBottom: '0.375rem',
83
- }}>
84
- {t.citedByLabel} ({citedByCount})
85
- </div>
86
- {citedBy.length > 0 && (
87
- <ul style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem', listStyle: 'none', padding: 0, margin: 0 }}>
88
- {citedBy.map(ref => (
89
- <li key={ref.resourceId} style={{ fontSize: 'var(--semiont-text-sm)', color: 'var(--semiont-text-primary)', fontWeight: 500 }}>
90
- {ref.resourceName}
91
- </li>
92
- ))}
93
- </ul>
94
- )}
95
- </div>
96
- )}
97
-
98
- </div>
176
+ <div className="semiont-gather-pane semiont-gather-pane--graph">
177
+ <div className="semiont-gather-pane__title">{t.graphPaneTitle}</div>
178
+ {context.inferredRelationshipSummary && (
179
+ <p className="semiont-gather-pane__summary">{context.inferredRelationshipSummary}</p>
180
+ )}
181
+ {!hasBody && (
182
+ <p className="semiont-gather-pane__empty">{t.graphEmpty}</p>
183
+ )}
184
+ {hasBody && (
185
+ <svg
186
+ className="semiont-graph"
187
+ viewBox={`0 0 ${width} ${height}`}
188
+ // Natural-size cap: 1 viewBox unit never exceeds 1px, so a compact
189
+ // graph renders small instead of stretching to fill the pane.
190
+ style={{ maxWidth: `${width}px` }}
191
+ role="img"
192
+ aria-label={t.graphPaneTitle}
193
+ >
194
+ {drawnEdges.map((e) => (
195
+ <g key={e.key} className={`semiont-graph__edge semiont-graph__edge--${e.kind}`}>
196
+ <line x1={e.x1} y1={e.y1} x2={e.x2} y2={e.y2} />
197
+ <title>{e.hover}</title>
198
+ </g>
199
+ ))}
200
+ {drawn.map((n) => (
201
+ <g
202
+ key={n.id}
203
+ className={`semiont-graph__node semiont-graph__node--${n.kind}`}
204
+ data-node-id={n.id}
205
+ >
206
+ <rect x={n.x} y={n.y} width={NODE_W} height={NODE_H} rx={6} />
207
+ <text x={n.x + 10} y={n.y + 22}>{truncate(n.label)}</text>
208
+ <title>{n.hover}</title>
209
+ </g>
210
+ ))}
211
+ </svg>
99
212
  )}
100
- </>
213
+ </div>
101
214
  );
102
215
  }