@semiont/react-ui 0.5.27 → 0.5.28

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 (162) hide show
  1. package/dist/{PdfAnnotationCanvas.client-EVUPTUWZ.js → PdfAnnotationCanvas.client-C2IGG4SW.js} +31 -4
  2. package/dist/{ar-OU3DQGC6.js → ar-FABPTJVB.js} +39 -23
  3. package/dist/{bn-PHWCR6AZ.js → bn-LKP6BDMH.js} +39 -23
  4. package/dist/{chunk-PCRRBU7H.js → chunk-2GOHTTHW.js} +39 -23
  5. package/dist/{chunk-5VDTWRXJ.js → chunk-2YHEE63S.js} +31 -31
  6. package/dist/{cs-NEXYZ62I.js → cs-YQYNWHRG.js} +39 -23
  7. package/dist/{da-FAT43YCU.js → da-QHZOYKRY.js} +39 -23
  8. package/dist/{de-KRQ3WBCU.js → de-GKA7RD7J.js} +39 -23
  9. package/dist/{el-Q4IU3EKC.js → el-45T257AL.js} +39 -23
  10. package/dist/{en-2KADE7N4.js → en-RVW5EQ66.js} +2 -2
  11. package/dist/{es-2IO3R2MK.js → es-AVER7RU4.js} +39 -23
  12. package/dist/{fa-Q5GOQFXX.js → fa-K3VPK3O2.js} +39 -23
  13. package/dist/{fi-R6NMRWBE.js → fi-5NEKF7EB.js} +39 -23
  14. package/dist/{fr-QY4D5E6J.js → fr-FUMNDLBZ.js} +39 -23
  15. package/dist/{he-X6Y77XHY.js → he-3JGNA3PR.js} +39 -23
  16. package/dist/{hi-XL7IOEQM.js → hi-IJY375DF.js} +39 -23
  17. package/dist/{id-QZ4ZZS67.js → id-LWHKJ7ZD.js} +39 -23
  18. package/dist/index.css +279 -74
  19. package/dist/index.d.ts +170 -145
  20. package/dist/index.js +2743 -2365
  21. package/dist/{it-ME2Q7OB6.js → it-EBVOV4RW.js} +39 -23
  22. package/dist/{ja-HX7QASRI.js → ja-KWRIX2NF.js} +39 -23
  23. package/dist/{ko-EQO5AHAV.js → ko-L5CJMFDS.js} +39 -23
  24. package/dist/{ms-UILFSF3I.js → ms-LSNMJDR2.js} +39 -23
  25. package/dist/{nl-NNI45PMK.js → nl-ZB32MB7U.js} +39 -23
  26. package/dist/{no-MAL5F6P6.js → no-4GTNY7M5.js} +39 -23
  27. package/dist/{pl-4RMY6JAJ.js → pl-LUHBLTHB.js} +39 -23
  28. package/dist/{pt-VQH2X3QQ.js → pt-C4H3UNQ6.js} +39 -23
  29. package/dist/{ro-XXCQO2OG.js → ro-GGITS5V4.js} +39 -23
  30. package/dist/{sv-7Z6EJ6U4.js → sv-T56RTLBP.js} +39 -23
  31. package/dist/test-utils.js +2 -2
  32. package/dist/{th-L3DXXYVQ.js → th-22X2IWCB.js} +39 -23
  33. package/dist/{tr-P3DFVLFJ.js → tr-77TFG62H.js} +39 -23
  34. package/dist/{uk-7C5D3PXS.js → uk-MIIL6NXT.js} +39 -23
  35. package/dist/{vi-ZLTLEO7P.js → vi-7JYOIGNV.js} +39 -23
  36. package/dist/{zh-5FHVWDIN.js → zh-ID6DRYU4.js} +39 -23
  37. package/package.json +9 -8
  38. package/src/components/AssistProgress.tsx +36 -2
  39. package/src/components/CodeMirrorRenderer.tsx +8 -8
  40. package/src/components/__tests__/AssistProgress.test.tsx +59 -0
  41. package/src/components/modals/ComposeStep.tsx +218 -0
  42. package/src/components/modals/ConfigureGatherStep.tsx +10 -18
  43. package/src/components/modals/ConfigureGenerationStep.tsx +35 -19
  44. package/src/components/modals/ConfigureSearchStep.tsx +19 -0
  45. package/src/components/modals/ContextSummary.tsx +185 -72
  46. package/src/components/modals/CorpusPane.tsx +75 -0
  47. package/src/components/modals/DiscardPrompt.tsx +38 -0
  48. package/src/components/modals/GatherContextStep.tsx +237 -107
  49. package/src/components/modals/ReferenceWizardModal.tsx +238 -50
  50. package/src/components/modals/ResourceGenerateModal.tsx +216 -108
  51. package/src/components/modals/SearchModal.css +337 -101
  52. package/src/components/modals/SearchResultsStep.tsx +61 -68
  53. package/src/components/modals/WizardFooter.tsx +7 -1
  54. package/src/components/modals/__tests__/ComposeStep.test.tsx +152 -0
  55. package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +14 -26
  56. package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +35 -0
  57. package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +24 -1
  58. package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +463 -25
  59. package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +417 -38
  60. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +276 -85
  61. package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
  62. package/src/components/modals/__tests__/WizardFooter.test.tsx +113 -4
  63. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +34 -1
  64. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +38 -1
  65. package/src/components/resource/AnnotateView.tsx +3 -3
  66. package/src/components/resource/BrowseView.tsx +5 -5
  67. package/src/components/resource/ResourceViewer.tsx +4 -4
  68. package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +1 -1
  69. package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +1 -1
  70. package/src/components/resource/__tests__/BrowseView.test.tsx +9 -9
  71. package/src/components/resource/annotate-renderers.tsx +3 -3
  72. package/src/components/resource/panels/AssistSection.css +35 -0
  73. package/src/components/resource/panels/ReferenceEntry.tsx +9 -2
  74. package/src/components/resource/panels/ReferencesPanel.tsx +4 -0
  75. package/src/components/resource/panels/ResourceInfoPanel.tsx +89 -20
  76. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +7 -2
  77. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +41 -0
  78. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +33 -1
  79. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +148 -6
  80. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +0 -65
  81. package/src/features/resource-compose/components/ResourceComposePage.tsx +17 -130
  82. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +1 -100
  83. package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -57
  84. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +71 -15
  85. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +133 -59
  86. package/src/styles/core/forms.css +35 -8
  87. package/src/styles/features/resource-viewer.css +0 -22
  88. package/src/styles/features/resource.css +0 -4
  89. package/translations/ar.json +38 -22
  90. package/translations/bn.json +38 -22
  91. package/translations/cs.json +38 -22
  92. package/translations/da.json +38 -22
  93. package/translations/de.json +38 -22
  94. package/translations/el.json +38 -22
  95. package/translations/en.json +38 -22
  96. package/translations/es.json +38 -22
  97. package/translations/fa.json +38 -22
  98. package/translations/fi.json +38 -22
  99. package/translations/fr.json +38 -22
  100. package/translations/he.json +38 -22
  101. package/translations/hi.json +38 -22
  102. package/translations/id.json +38 -22
  103. package/translations/it.json +38 -22
  104. package/translations/ja.json +38 -22
  105. package/translations/ko.json +38 -22
  106. package/translations/ms.json +38 -22
  107. package/translations/nl.json +38 -22
  108. package/translations/no.json +38 -22
  109. package/translations/pl.json +38 -22
  110. package/translations/pt.json +38 -22
  111. package/translations/ro.json +38 -22
  112. package/translations/sv.json +38 -22
  113. package/translations/th.json +38 -22
  114. package/translations/tr.json +38 -22
  115. package/translations/uk.json +38 -22
  116. package/translations/vi.json +38 -22
  117. package/translations/zh.json +38 -22
  118. package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js.map +0 -1
  119. package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css.map +0 -1
  120. package/dist/ar-OU3DQGC6.js.map +0 -1
  121. package/dist/bn-PHWCR6AZ.js.map +0 -1
  122. package/dist/chunk-5VDTWRXJ.js.map +0 -1
  123. package/dist/chunk-BTUG3VJJ.js.map +0 -1
  124. package/dist/chunk-O23TLLXW.js.map +0 -1
  125. package/dist/chunk-PCRRBU7H.js.map +0 -1
  126. package/dist/chunk-QT7LYM72.js.map +0 -1
  127. package/dist/chunk-VXASXU4K.js.map +0 -1
  128. package/dist/cs-NEXYZ62I.js.map +0 -1
  129. package/dist/da-FAT43YCU.js.map +0 -1
  130. package/dist/de-KRQ3WBCU.js.map +0 -1
  131. package/dist/el-Q4IU3EKC.js.map +0 -1
  132. package/dist/en-2KADE7N4.js.map +0 -1
  133. package/dist/es-2IO3R2MK.js.map +0 -1
  134. package/dist/fa-Q5GOQFXX.js.map +0 -1
  135. package/dist/fi-R6NMRWBE.js.map +0 -1
  136. package/dist/fr-QY4D5E6J.js.map +0 -1
  137. package/dist/he-X6Y77XHY.js.map +0 -1
  138. package/dist/hi-XL7IOEQM.js.map +0 -1
  139. package/dist/id-QZ4ZZS67.js.map +0 -1
  140. package/dist/index.css.map +0 -1
  141. package/dist/index.js.map +0 -1
  142. package/dist/integrations/css-modules-helper.js.map +0 -1
  143. package/dist/integrations/styled-components-theme.js.map +0 -1
  144. package/dist/it-ME2Q7OB6.js.map +0 -1
  145. package/dist/ja-HX7QASRI.js.map +0 -1
  146. package/dist/ko-EQO5AHAV.js.map +0 -1
  147. package/dist/ms-UILFSF3I.js.map +0 -1
  148. package/dist/nl-NNI45PMK.js.map +0 -1
  149. package/dist/no-MAL5F6P6.js.map +0 -1
  150. package/dist/pl-4RMY6JAJ.js.map +0 -1
  151. package/dist/pt-VQH2X3QQ.js.map +0 -1
  152. package/dist/ro-XXCQO2OG.js.map +0 -1
  153. package/dist/sv-7Z6EJ6U4.js.map +0 -1
  154. package/dist/test-utils.css.map +0 -1
  155. package/dist/test-utils.js.map +0 -1
  156. package/dist/th-L3DXXYVQ.js.map +0 -1
  157. package/dist/tr-P3DFVLFJ.js.map +0 -1
  158. package/dist/uk-7C5D3PXS.js.map +0 -1
  159. package/dist/vi-ZLTLEO7P.js.map +0 -1
  160. package/dist/zh-5FHVWDIN.js.map +0 -1
  161. package/src/components/modals/ResourceSearchModal.tsx +0 -224
  162. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +0 -163
@@ -35,14 +35,18 @@ export interface GenerationConfig {
35
35
 
36
36
  export interface ConfigureGenerationStepProps {
37
37
  context: GatheredContext;
38
+ /** Echo of the gather step's hint — the thing being steered stays visible (GEP D8). */
39
+ hintEcho?: { label: string; value: string };
38
40
  /** Owned by the wizard so Back is lossless (WIZARD-NAVIGATION D3). */
39
41
  config: GenerationDraft;
40
42
  onConfigChange: (config: GenerationDraft) => void;
41
- onBack: () => void;
43
+ /** Absent in a single-stack host (GATHER-AT-THE-TOP D6) the footer then renders no retreat. */
44
+ onBack?: () => void;
42
45
  onGenerate: (config: GenerationConfig) => void;
43
46
  translations: {
44
47
  resourceTitle: string;
45
48
  resourceTitlePlaceholder: string;
49
+ saveLocation: string;
46
50
  additionalInstructions: string;
47
51
  additionalInstructionsPlaceholder: string;
48
52
  language: string;
@@ -58,7 +62,7 @@ export interface ConfigureGenerationStepProps {
58
62
  * and `{{model}}`.
59
63
  */
60
64
  maxLengthCeiling: string;
61
- back: string;
65
+ back?: string;
62
66
  generate: string;
63
67
  };
64
68
  /**
@@ -73,6 +77,7 @@ export interface ConfigureGenerationStepProps {
73
77
 
74
78
  export function ConfigureGenerationStep({
75
79
  context,
80
+ hintEcho,
76
81
  config,
77
82
  onConfigChange,
78
83
  onBack,
@@ -127,7 +132,15 @@ export function ConfigureGenerationStep({
127
132
  };
128
133
 
129
134
  return (
130
- <form onSubmit={handleSubmit} className="semiont-form semiont-form--scrollable">
135
+ // No --scrollable: this form renders below the evidence display inside the
136
+ // host's single step-scroll pane — an independent scroll region here is
137
+ // what squeezed the parameters out of view (the measured failure).
138
+ <form onSubmit={handleSubmit} className="semiont-form">
139
+ {hintEcho && (
140
+ <p className="semiont-wizard__hint-echo">
141
+ {hintEcho.label}: {hintEcho.value}
142
+ </p>
143
+ )}
131
144
  {/* Resource Title */}
132
145
  <div className="semiont-form__field">
133
146
  <label htmlFor="wizard-title" className="semiont-form__label">
@@ -147,7 +160,7 @@ export function ConfigureGenerationStep({
147
160
  {/* Storage URI */}
148
161
  <div className="semiont-form__field">
149
162
  <label htmlFor="wizard-storagePath" className="semiont-form__label">
150
- Save location
163
+ {t.saveLocation}
151
164
  </label>
152
165
  <div className="semiont-input-addon">
153
166
  <span className="semiont-input-addon__prefix">file://</span>
@@ -198,7 +211,7 @@ export function ConfigureGenerationStep({
198
211
  </select>
199
212
  </div>
200
213
 
201
- <div className="semiont-form__field semiont-form__field--inline semiont-form__field--grow">
214
+ <div className="semiont-form__field semiont-form__field--inline semiont-form__field--slider">
202
215
  <label htmlFor="wizard-temperature" className="semiont-form__label">
203
216
  {t.creativity} ({temperature.toFixed(1)})
204
217
  </label>
@@ -218,7 +231,7 @@ export function ConfigureGenerationStep({
218
231
  </div>
219
232
  </div>
220
233
 
221
- <div className="semiont-form__field semiont-form__field--inline semiont-form__field--narrow">
234
+ <div className="semiont-form__field semiont-form__field--inline semiont-form__field--narrow semiont-form__field--end">
222
235
  <label htmlFor="wizard-maxTokens" className="semiont-form__label">
223
236
  {t.maxLength}
224
237
  </label>
@@ -242,23 +255,26 @@ export function ConfigureGenerationStep({
242
255
  }}
243
256
  className="semiont-input"
244
257
  />
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
258
  </div>
257
259
  </div>
258
260
 
261
+ {/* The bound, as a full sentence — which is why it lives UNDER the row,
262
+ never inside the 5.5rem Max Length column: a sentence in that box
263
+ wraps one word per line and its height taxes the whole flex row. */}
264
+ <p className="semiont-form__help semiont-form__help--row">
265
+ {generationAgent?.limits
266
+ ? t.maxLengthCeiling
267
+ .replace('{{maxOutputTokens}}', String(ceiling))
268
+ .replace(
269
+ '{{model}}',
270
+ (generationAgent.agent as { model?: string; name?: string }).model
271
+ ?? generationAgent.agent.name,
272
+ )
273
+ : t.maxLengthHelp}
274
+ </p>
275
+
259
276
  <WizardFooter
260
- backLabel={t.back}
261
- onBack={onBack}
277
+ {...(onBack && t.back ? { backLabel: t.back, onBack } : {})}
262
278
  primary={{ label: t.generate, type: 'submit' }}
263
279
  />
264
280
  </form>
@@ -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
  }
@@ -0,0 +1,75 @@
1
+ 'use client';
2
+
3
+ import type { GatheredContext } from '@semiont/core';
4
+
5
+ export interface CorpusPaneTranslations {
6
+ corpusPaneTitle: string;
7
+ /** Strategy-relevant empty state — emptiness is evidence (GEP D1). */
8
+ corpusEmpty: string;
9
+ /** Label for the match's cosine score. */
10
+ score: string;
11
+ /** The filter receipt; interpolates `{{types}}`. */
12
+ excludedReceipt: string;
13
+ /** The trust marker for machine-read (OCR) sources. */
14
+ machineRead: string;
15
+ }
16
+
17
+ export interface CorpusPaneProps {
18
+ semanticContext: GatheredContext['semanticContext'];
19
+ translations: CorpusPaneTranslations;
20
+ }
21
+
22
+ /**
23
+ * The corpus pane (GEP D4): latent knowledge — ranked `SemanticMatch` cards,
24
+ * read-only. THE datum for Search-vs-Generate ("does the KB already discuss
25
+ * this?"). This is recall-at-gather evidence, not a preview of Search: the
26
+ * matcher re-runs fresh (selected text + hint, scoring) and may rank
27
+ * differently.
28
+ *
29
+ * Read-only for now by decision, not forever — when per-match actions arrive
30
+ * they route through the footer's fork or a designed extension of it, never
31
+ * ad-hoc buttons here.
32
+ */
33
+ export function CorpusPane({ semanticContext, translations: t }: CorpusPaneProps) {
34
+ // Descending by score, whatever order the wire delivered.
35
+ const matches = [...(semanticContext?.similar ?? [])].sort((a, b) => b.score - a.score);
36
+ const excluded = semanticContext?.excludedEntityTypes ?? [];
37
+
38
+ return (
39
+ <div className="semiont-gather-pane semiont-gather-pane--corpus">
40
+ <div className="semiont-gather-pane__title">{t.corpusPaneTitle}</div>
41
+ {matches.length === 0 ? (
42
+ <p className="semiont-gather-pane__empty">{t.corpusEmpty}</p>
43
+ ) : (
44
+ <ul className="semiont-corpus__list">
45
+ {matches.map((m, i) => (
46
+ <li key={`${m.resourceId}-${i}`} className="semiont-corpus__card">
47
+ <div className="semiont-corpus__card-head">
48
+ <span className="semiont-corpus__source">{m.resourceName}</span>
49
+ {(m.entityTypes ?? []).map((et) => (
50
+ <span key={et} className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.375rem', fontWeight: 400 }}>
51
+ {et}
52
+ </span>
53
+ ))}
54
+ {m.machineRead && (
55
+ <span className="semiont-chip semiont-corpus__trust" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.375rem', fontWeight: 400 }}>
56
+ {t.machineRead}
57
+ </span>
58
+ )}
59
+ {/* 0–1 cosine to two places — deliberately NOT the search step's
60
+ raw render, which is a different scale (logged deviation). */}
61
+ <span className="semiont-corpus__score">{t.score}: {m.score.toFixed(2)}</span>
62
+ </div>
63
+ <p className="semiont-corpus__snippet">{m.text}</p>
64
+ </li>
65
+ ))}
66
+ </ul>
67
+ )}
68
+ {excluded.length > 0 && (
69
+ <p className="semiont-corpus__receipt">
70
+ {t.excludedReceipt.replace('{{types}}', excluded.join(', '))}
71
+ </p>
72
+ )}
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * The inline discard band (COMPOSE-IN-MODAL D4, shared by GATHER-AT-THE-TOP
5
+ * P1): a modal dies on ✕/Escape/backdrop, but typed work must not die with
6
+ * it. Hosts route ALL dismissal through one handler; when the draft is dirty
7
+ * that handler shows this band instead of closing. Success paths never come
8
+ * here — the guard protects dismissal, not completion.
9
+ */
10
+ interface DiscardPromptProps {
11
+ prompt: string;
12
+ discardLabel: string;
13
+ keepLabel: string;
14
+ onDiscard: () => void;
15
+ onKeepEditing: () => void;
16
+ }
17
+
18
+ export function DiscardPrompt({ prompt, discardLabel, keepLabel, onDiscard, onKeepEditing }: DiscardPromptProps) {
19
+ return (
20
+ <div className="semiont-wizard__discard-prompt" role="alert">
21
+ <span className="semiont-wizard__discard-prompt-text">{prompt}</span>
22
+ <button
23
+ type="button"
24
+ className="semiont-button--danger"
25
+ onClick={onDiscard}
26
+ >
27
+ {discardLabel}
28
+ </button>
29
+ <button
30
+ type="button"
31
+ className="semiont-button--secondary"
32
+ onClick={onKeepEditing}
33
+ >
34
+ {keepLabel}
35
+ </button>
36
+ </div>
37
+ );
38
+ }