@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.
- package/dist/{PdfAnnotationCanvas.client-EVUPTUWZ.js → PdfAnnotationCanvas.client-C2IGG4SW.js} +31 -4
- package/dist/{ar-OU3DQGC6.js → ar-FABPTJVB.js} +39 -23
- package/dist/{bn-PHWCR6AZ.js → bn-LKP6BDMH.js} +39 -23
- package/dist/{chunk-PCRRBU7H.js → chunk-2GOHTTHW.js} +39 -23
- package/dist/{chunk-5VDTWRXJ.js → chunk-2YHEE63S.js} +31 -31
- package/dist/{cs-NEXYZ62I.js → cs-YQYNWHRG.js} +39 -23
- package/dist/{da-FAT43YCU.js → da-QHZOYKRY.js} +39 -23
- package/dist/{de-KRQ3WBCU.js → de-GKA7RD7J.js} +39 -23
- package/dist/{el-Q4IU3EKC.js → el-45T257AL.js} +39 -23
- package/dist/{en-2KADE7N4.js → en-RVW5EQ66.js} +2 -2
- package/dist/{es-2IO3R2MK.js → es-AVER7RU4.js} +39 -23
- package/dist/{fa-Q5GOQFXX.js → fa-K3VPK3O2.js} +39 -23
- package/dist/{fi-R6NMRWBE.js → fi-5NEKF7EB.js} +39 -23
- package/dist/{fr-QY4D5E6J.js → fr-FUMNDLBZ.js} +39 -23
- package/dist/{he-X6Y77XHY.js → he-3JGNA3PR.js} +39 -23
- package/dist/{hi-XL7IOEQM.js → hi-IJY375DF.js} +39 -23
- package/dist/{id-QZ4ZZS67.js → id-LWHKJ7ZD.js} +39 -23
- package/dist/index.css +279 -74
- package/dist/index.d.ts +170 -145
- package/dist/index.js +2743 -2365
- package/dist/{it-ME2Q7OB6.js → it-EBVOV4RW.js} +39 -23
- package/dist/{ja-HX7QASRI.js → ja-KWRIX2NF.js} +39 -23
- package/dist/{ko-EQO5AHAV.js → ko-L5CJMFDS.js} +39 -23
- package/dist/{ms-UILFSF3I.js → ms-LSNMJDR2.js} +39 -23
- package/dist/{nl-NNI45PMK.js → nl-ZB32MB7U.js} +39 -23
- package/dist/{no-MAL5F6P6.js → no-4GTNY7M5.js} +39 -23
- package/dist/{pl-4RMY6JAJ.js → pl-LUHBLTHB.js} +39 -23
- package/dist/{pt-VQH2X3QQ.js → pt-C4H3UNQ6.js} +39 -23
- package/dist/{ro-XXCQO2OG.js → ro-GGITS5V4.js} +39 -23
- package/dist/{sv-7Z6EJ6U4.js → sv-T56RTLBP.js} +39 -23
- package/dist/test-utils.js +2 -2
- package/dist/{th-L3DXXYVQ.js → th-22X2IWCB.js} +39 -23
- package/dist/{tr-P3DFVLFJ.js → tr-77TFG62H.js} +39 -23
- package/dist/{uk-7C5D3PXS.js → uk-MIIL6NXT.js} +39 -23
- package/dist/{vi-ZLTLEO7P.js → vi-7JYOIGNV.js} +39 -23
- package/dist/{zh-5FHVWDIN.js → zh-ID6DRYU4.js} +39 -23
- package/package.json +9 -8
- package/src/components/AssistProgress.tsx +36 -2
- package/src/components/CodeMirrorRenderer.tsx +8 -8
- package/src/components/__tests__/AssistProgress.test.tsx +59 -0
- package/src/components/modals/ComposeStep.tsx +218 -0
- package/src/components/modals/ConfigureGatherStep.tsx +10 -18
- package/src/components/modals/ConfigureGenerationStep.tsx +35 -19
- package/src/components/modals/ConfigureSearchStep.tsx +19 -0
- package/src/components/modals/ContextSummary.tsx +185 -72
- package/src/components/modals/CorpusPane.tsx +75 -0
- package/src/components/modals/DiscardPrompt.tsx +38 -0
- package/src/components/modals/GatherContextStep.tsx +237 -107
- package/src/components/modals/ReferenceWizardModal.tsx +238 -50
- package/src/components/modals/ResourceGenerateModal.tsx +216 -108
- package/src/components/modals/SearchModal.css +337 -101
- package/src/components/modals/SearchResultsStep.tsx +61 -68
- package/src/components/modals/WizardFooter.tsx +7 -1
- package/src/components/modals/__tests__/ComposeStep.test.tsx +152 -0
- package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +14 -26
- package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +35 -0
- package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +24 -1
- package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +463 -25
- package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +417 -38
- package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +276 -85
- package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
- package/src/components/modals/__tests__/WizardFooter.test.tsx +113 -4
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +34 -1
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +38 -1
- package/src/components/resource/AnnotateView.tsx +3 -3
- package/src/components/resource/BrowseView.tsx +5 -5
- package/src/components/resource/ResourceViewer.tsx +4 -4
- package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +1 -1
- package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +1 -1
- package/src/components/resource/__tests__/BrowseView.test.tsx +9 -9
- package/src/components/resource/annotate-renderers.tsx +3 -3
- package/src/components/resource/panels/AssistSection.css +35 -0
- package/src/components/resource/panels/ReferenceEntry.tsx +9 -2
- package/src/components/resource/panels/ReferencesPanel.tsx +4 -0
- package/src/components/resource/panels/ResourceInfoPanel.tsx +89 -20
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +7 -2
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +41 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +33 -1
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +148 -6
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +0 -65
- package/src/features/resource-compose/components/ResourceComposePage.tsx +17 -130
- package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +1 -100
- package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -57
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +71 -15
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +133 -59
- package/src/styles/core/forms.css +35 -8
- package/src/styles/features/resource-viewer.css +0 -22
- package/src/styles/features/resource.css +0 -4
- package/translations/ar.json +38 -22
- package/translations/bn.json +38 -22
- package/translations/cs.json +38 -22
- package/translations/da.json +38 -22
- package/translations/de.json +38 -22
- package/translations/el.json +38 -22
- package/translations/en.json +38 -22
- package/translations/es.json +38 -22
- package/translations/fa.json +38 -22
- package/translations/fi.json +38 -22
- package/translations/fr.json +38 -22
- package/translations/he.json +38 -22
- package/translations/hi.json +38 -22
- package/translations/id.json +38 -22
- package/translations/it.json +38 -22
- package/translations/ja.json +38 -22
- package/translations/ko.json +38 -22
- package/translations/ms.json +38 -22
- package/translations/nl.json +38 -22
- package/translations/no.json +38 -22
- package/translations/pl.json +38 -22
- package/translations/pt.json +38 -22
- package/translations/ro.json +38 -22
- package/translations/sv.json +38 -22
- package/translations/th.json +38 -22
- package/translations/tr.json +38 -22
- package/translations/uk.json +38 -22
- package/translations/vi.json +38 -22
- package/translations/zh.json +38 -22
- package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js.map +0 -1
- package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css.map +0 -1
- package/dist/ar-OU3DQGC6.js.map +0 -1
- package/dist/bn-PHWCR6AZ.js.map +0 -1
- package/dist/chunk-5VDTWRXJ.js.map +0 -1
- package/dist/chunk-BTUG3VJJ.js.map +0 -1
- package/dist/chunk-O23TLLXW.js.map +0 -1
- package/dist/chunk-PCRRBU7H.js.map +0 -1
- package/dist/chunk-QT7LYM72.js.map +0 -1
- package/dist/chunk-VXASXU4K.js.map +0 -1
- package/dist/cs-NEXYZ62I.js.map +0 -1
- package/dist/da-FAT43YCU.js.map +0 -1
- package/dist/de-KRQ3WBCU.js.map +0 -1
- package/dist/el-Q4IU3EKC.js.map +0 -1
- package/dist/en-2KADE7N4.js.map +0 -1
- package/dist/es-2IO3R2MK.js.map +0 -1
- package/dist/fa-Q5GOQFXX.js.map +0 -1
- package/dist/fi-R6NMRWBE.js.map +0 -1
- package/dist/fr-QY4D5E6J.js.map +0 -1
- package/dist/he-X6Y77XHY.js.map +0 -1
- package/dist/hi-XL7IOEQM.js.map +0 -1
- package/dist/id-QZ4ZZS67.js.map +0 -1
- package/dist/index.css.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integrations/css-modules-helper.js.map +0 -1
- package/dist/integrations/styled-components-theme.js.map +0 -1
- package/dist/it-ME2Q7OB6.js.map +0 -1
- package/dist/ja-HX7QASRI.js.map +0 -1
- package/dist/ko-EQO5AHAV.js.map +0 -1
- package/dist/ms-UILFSF3I.js.map +0 -1
- package/dist/nl-NNI45PMK.js.map +0 -1
- package/dist/no-MAL5F6P6.js.map +0 -1
- package/dist/pl-4RMY6JAJ.js.map +0 -1
- package/dist/pt-VQH2X3QQ.js.map +0 -1
- package/dist/ro-XXCQO2OG.js.map +0 -1
- package/dist/sv-7Z6EJ6U4.js.map +0 -1
- package/dist/test-utils.css.map +0 -1
- package/dist/test-utils.js.map +0 -1
- package/dist/th-L3DXXYVQ.js.map +0 -1
- package/dist/tr-P3DFVLFJ.js.map +0 -1
- package/dist/uk-7C5D3PXS.js.map +0 -1
- package/dist/vi-ZLTLEO7P.js.map +0 -1
- package/dist/zh-5FHVWDIN.js.map +0 -1
- package/src/components/modals/ResourceSearchModal.tsx +0 -224
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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--
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
?
|
|
23
|
-
:
|
|
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
|
|
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
|
-
{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
}
|