@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.
- package/README.md +3 -3
- package/dist/{PdfAnnotationCanvas.client-EVUPTUWZ.js → PdfAnnotationCanvas.client-LSH3PZPY.js} +32 -5
- package/dist/{ar-OU3DQGC6.js → ar-AZBRADEB.js} +43 -23
- package/dist/{bn-PHWCR6AZ.js → bn-CCEW6AOS.js} +43 -23
- package/dist/{chunk-VXASXU4K.js → chunk-3ZBKCWUZ.js} +1 -1
- package/dist/{chunk-5VDTWRXJ.js → chunk-G4VHL4UA.js} +31 -31
- package/dist/{chunk-PCRRBU7H.js → chunk-SDKFZLEX.js} +43 -23
- package/dist/{cs-NEXYZ62I.js → cs-HXPF2CTE.js} +43 -23
- package/dist/{da-FAT43YCU.js → da-K5NTK7LH.js} +43 -23
- package/dist/{de-KRQ3WBCU.js → de-GMRQEDZI.js} +43 -23
- package/dist/{el-Q4IU3EKC.js → el-EUB4CI4G.js} +43 -23
- package/dist/{en-2KADE7N4.js → en-R5XA26NF.js} +2 -2
- package/dist/{es-2IO3R2MK.js → es-Z32DAZ6W.js} +43 -23
- package/dist/{fa-Q5GOQFXX.js → fa-G7HN6B32.js} +43 -23
- package/dist/{fi-R6NMRWBE.js → fi-3YBLZO7I.js} +43 -23
- package/dist/{fr-QY4D5E6J.js → fr-VUBH7BBS.js} +43 -23
- package/dist/{he-X6Y77XHY.js → he-WQQEY26B.js} +43 -23
- package/dist/{hi-XL7IOEQM.js → hi-HJZ6W4JB.js} +43 -23
- package/dist/{id-QZ4ZZS67.js → id-ZOGZ37MI.js} +43 -23
- package/dist/index.css +279 -74
- package/dist/index.d.ts +318 -393
- package/dist/index.js +2619 -2687
- package/dist/{it-ME2Q7OB6.js → it-DT6UW66F.js} +43 -23
- package/dist/{ja-HX7QASRI.js → ja-RUKQCOLO.js} +43 -23
- package/dist/{ko-EQO5AHAV.js → ko-ZH5ADKZM.js} +43 -23
- package/dist/{ms-UILFSF3I.js → ms-Y2AFPZTU.js} +43 -23
- package/dist/{nl-NNI45PMK.js → nl-OQEWIXNA.js} +43 -23
- package/dist/{no-MAL5F6P6.js → no-KPAYJZWY.js} +43 -23
- package/dist/{pl-4RMY6JAJ.js → pl-WSXK2UUD.js} +43 -23
- package/dist/{pt-VQH2X3QQ.js → pt-NWRJRUCK.js} +43 -23
- package/dist/{ro-XXCQO2OG.js → ro-3CGVMOC6.js} +43 -23
- package/dist/{sv-7Z6EJ6U4.js → sv-HGSEBA7S.js} +43 -23
- package/dist/test-utils.js +16 -7
- package/dist/{th-L3DXXYVQ.js → th-NZ4G352Z.js} +43 -23
- package/dist/{tr-P3DFVLFJ.js → tr-LGGS4DLW.js} +43 -23
- package/dist/{uk-7C5D3PXS.js → uk-AIX536L4.js} +43 -23
- package/dist/{vi-ZLTLEO7P.js → vi-V6CVQZLQ.js} +43 -23
- package/dist/{zh-5FHVWDIN.js → zh-AGKPGI3L.js} +43 -23
- package/package.json +16 -15
- package/src/components/AssistProgress.tsx +36 -2
- package/src/components/CodeMirrorRenderer.tsx +8 -8
- package/src/components/StatusDisplay.tsx +13 -13
- package/src/components/__tests__/AssistProgress.test.tsx +59 -0
- package/src/components/__tests__/StatusDisplay.test.tsx +9 -9
- package/src/components/modals/ComposeStep.tsx +233 -0
- package/src/components/modals/ConfigureGatherStep.tsx +10 -18
- package/src/components/modals/ConfigureGenerationStep.tsx +178 -34
- 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 +252 -59
- package/src/components/modals/ResourceGenerateModal.tsx +225 -112
- 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 +206 -8
- 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 +442 -39
- package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +300 -86
- package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
- package/src/components/modals/__tests__/WizardFooter.test.tsx +116 -9
- 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 +8 -8
- package/src/components/resource/DownloadFileLink.tsx +1 -1
- 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.reference-hover.test.tsx +1 -1
- package/src/components/resource/__tests__/BrowseView.test.tsx +11 -11
- package/src/components/resource/__tests__/download-link.test.tsx +6 -6
- 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 +11 -4
- package/src/components/resource/panels/ReferencesPanel.tsx +9 -1
- 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.observable-flow.test.tsx +1 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +84 -3
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +149 -7
- package/src/features/auth/__tests__/SignInForm.a11y.test.tsx +12 -12
- package/src/features/auth/components/SignInForm.tsx +53 -53
- package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +2 -2
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +0 -65
- package/src/features/resource-compose/components/ResourceComposePage.tsx +35 -133
- 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-discovery/__tests__/ResourceDiscoveryPage.test.tsx +1 -1
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +2 -2
- package/src/features/resource-discovery/state/discover-state-unit.ts +1 -1
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +73 -17
- package/src/features/resource-viewer/__tests__/generation-options.test.ts +53 -0
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +148 -83
- package/src/features/resource-viewer/generation-options.ts +29 -0
- 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/src/styles/index.css +0 -1
- package/translations/ar.json +42 -22
- package/translations/bn.json +42 -22
- package/translations/cs.json +42 -22
- package/translations/da.json +42 -22
- package/translations/de.json +42 -22
- package/translations/el.json +42 -22
- package/translations/en.json +42 -22
- package/translations/es.json +42 -22
- package/translations/fa.json +42 -22
- package/translations/fi.json +42 -22
- package/translations/fr.json +42 -22
- package/translations/he.json +42 -22
- package/translations/hi.json +42 -22
- package/translations/id.json +42 -22
- package/translations/it.json +42 -22
- package/translations/ja.json +42 -22
- package/translations/ko.json +42 -22
- package/translations/ms.json +42 -22
- package/translations/nl.json +42 -22
- package/translations/no.json +42 -22
- package/translations/pl.json +42 -22
- package/translations/pt.json +42 -22
- package/translations/ro.json +42 -22
- package/translations/sv.json +42 -22
- package/translations/th.json +42 -22
- package/translations/tr.json +42 -22
- package/translations/uk.json +42 -22
- package/translations/vi.json +42 -22
- package/translations/zh.json +42 -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
- package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +0 -137
- package/src/features/admin-exchange/__tests__/ExportCard.test.tsx +0 -41
- package/src/features/admin-exchange/__tests__/ImportCard.test.tsx +0 -148
- package/src/features/admin-exchange/__tests__/ImportProgress.test.tsx +0 -117
- package/src/features/admin-exchange/components/AdminExchangePage.tsx +0 -117
- package/src/features/admin-exchange/components/ExportCard.tsx +0 -35
- package/src/features/admin-exchange/components/ImportCard.tsx +0 -183
- package/src/features/admin-exchange/components/ImportProgress.tsx +0 -80
- package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +0 -185
- package/src/features/admin-exchange/state/exchange-state-unit.ts +0 -133
- package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +0 -118
- package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +0 -118
- 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 {
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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={
|
|
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--
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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
|
}
|