@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
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
4
|
-
import type
|
|
4
|
+
import { readyValue, type SemiontSession, type YieldOutcome } from '@semiont/sdk';
|
|
5
5
|
import { formatLocaleDisplay } from '@semiont/core';
|
|
6
6
|
import { resourceId as makeResourceId, type components } from '@semiont/core';
|
|
7
|
+
import { useObservable } from '../../../hooks/useObservable';
|
|
7
8
|
import { renderAgentLabel } from './agent-label';
|
|
9
|
+
import { AssistShell } from './AssistShell';
|
|
10
|
+
import { assistProgressTranslations } from '../../../lib/assist-progress-copy';
|
|
8
11
|
import './ResourceInfoPanel.css';
|
|
9
12
|
|
|
10
13
|
type Agent = components['schemas']['Agent'];
|
|
14
|
+
type JobProgress = components['schemas']['JobProgress'];
|
|
11
15
|
|
|
12
16
|
interface Props {
|
|
13
17
|
/** Session carrying the client and event bus; null renders inert. */
|
|
@@ -30,6 +34,17 @@ interface Props {
|
|
|
30
34
|
* to a verb). Omit to hide the Generate action.
|
|
31
35
|
*/
|
|
32
36
|
onGenerate?: () => void;
|
|
37
|
+
/**
|
|
38
|
+
* Generation lifecycle, page-held (`yield.isGenerating$` / `yield.progress$`
|
|
39
|
+
* / `yield.outcome$`): this panel is generation's progress surface
|
|
40
|
+
* (GENERATE-FROM-RESOURCE D7) — the run reports where it was started.
|
|
41
|
+
*/
|
|
42
|
+
isGenerating?: boolean;
|
|
43
|
+
generationProgress?: JobProgress | null;
|
|
44
|
+
/** The finished run's result; the ended frame links it by name (D8). */
|
|
45
|
+
generationOutcome?: YieldOutcome | null;
|
|
46
|
+
/** Clear the finished display — wires `yield.dismissProgress()`. */
|
|
47
|
+
onDismissProgress?: () => void;
|
|
33
48
|
}
|
|
34
49
|
|
|
35
50
|
/**
|
|
@@ -54,8 +69,13 @@ export function ResourceInfoPanel({
|
|
|
54
69
|
wasDerivedFrom,
|
|
55
70
|
generator,
|
|
56
71
|
onGenerate,
|
|
72
|
+
isGenerating = false,
|
|
73
|
+
generationProgress = null,
|
|
74
|
+
generationOutcome = null,
|
|
75
|
+
onDismissProgress,
|
|
57
76
|
}: Props) {
|
|
58
77
|
const t = useTranslations('ResourceInfoPanel');
|
|
78
|
+
const ta = useTranslations('AssistProgress');
|
|
59
79
|
|
|
60
80
|
// Single attribution surface. `wasAttributedTo` is the canonical list
|
|
61
81
|
// of responsible parties; if a producer set only `generator` we
|
|
@@ -152,13 +172,7 @@ export function ResourceInfoPanel({
|
|
|
152
172
|
<span className="semiont-resource-info-panel__label">{t('derivedFrom')}</span>
|
|
153
173
|
<span className="semiont-resource-info-panel__value">
|
|
154
174
|
{(Array.isArray(wasDerivedFrom) ? wasDerivedFrom : [wasDerivedFrom]).map((id, i) => (
|
|
155
|
-
<
|
|
156
|
-
key={id}
|
|
157
|
-
className="semiont-resource-info-panel__link"
|
|
158
|
-
onClick={() => session?.client.browse.openResource(makeResourceId(id))}
|
|
159
|
-
>
|
|
160
|
-
{i > 0 && ', '}{id}
|
|
161
|
-
</button>
|
|
175
|
+
<DerivedFromLink key={id} session={session} id={id} first={i === 0} />
|
|
162
176
|
))}
|
|
163
177
|
</span>
|
|
164
178
|
</div>
|
|
@@ -185,19 +199,47 @@ export function ResourceInfoPanel({
|
|
|
185
199
|
</div>
|
|
186
200
|
)}
|
|
187
201
|
|
|
188
|
-
{/* Generate
|
|
202
|
+
{/* Generate — the run reports where it was started (GENERATE-FROM-RESOURCE
|
|
203
|
+
D7): the same AssistShell every assist uses, its form the Generate
|
|
204
|
+
control, the progress frame in its place while a generation runs. */}
|
|
189
205
|
{onGenerate && (
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
{
|
|
199
|
-
|
|
200
|
-
|
|
206
|
+
<AssistShell
|
|
207
|
+
assistType="generation"
|
|
208
|
+
title={t('generate')}
|
|
209
|
+
isAssisting={isGenerating}
|
|
210
|
+
progress={generationProgress}
|
|
211
|
+
progressProps={{
|
|
212
|
+
onCancel: () => session?.client.job.cancelRequest('generation'),
|
|
213
|
+
...(onDismissProgress ? { onDismiss: onDismissProgress } : {}),
|
|
214
|
+
...(generationOutcome ? {
|
|
215
|
+
outcome: {
|
|
216
|
+
label: generationOutcome.resourceName,
|
|
217
|
+
onOpen: () => session?.client.browse.openResource(generationOutcome.resourceId),
|
|
218
|
+
},
|
|
219
|
+
// The terminal sentence derives from the OUTCOME, not the final
|
|
220
|
+
// progress frame — whose fire-and-forget emit can lose the race
|
|
221
|
+
// with job:complete and leave "creating…" beside a ✅
|
|
222
|
+
// (GENERATION-ARRIVAL D4/D5).
|
|
223
|
+
endedMessage: generationOutcome.truncated
|
|
224
|
+
? ta('codeCompleteGeneratedTruncated')
|
|
225
|
+
: ta('codeCompleteGenerated'),
|
|
226
|
+
} : {}),
|
|
227
|
+
translations: assistProgressTranslations(ta),
|
|
228
|
+
}}
|
|
229
|
+
form={
|
|
230
|
+
<>
|
|
231
|
+
<button
|
|
232
|
+
onClick={onGenerate}
|
|
233
|
+
className="semiont-resource-button semiont-resource-button--secondary"
|
|
234
|
+
>
|
|
235
|
+
✨ {t('generate')}
|
|
236
|
+
</button>
|
|
237
|
+
<p className="semiont-resource-info-panel__description">
|
|
238
|
+
{t('generateDescription')}
|
|
239
|
+
</p>
|
|
240
|
+
</>
|
|
241
|
+
}
|
|
242
|
+
/>
|
|
201
243
|
)}
|
|
202
244
|
|
|
203
245
|
{/* Clone Action */}
|
|
@@ -244,3 +286,30 @@ export function ResourceInfoPanel({
|
|
|
244
286
|
</div>
|
|
245
287
|
);
|
|
246
288
|
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* One derived-from entry: linked by id (wire identity), labeled by the source
|
|
292
|
+
* resource's NAME once `browse.resource` serves its descriptor. The raw id is
|
|
293
|
+
* the honest label until then — and stays if the descriptor never resolves
|
|
294
|
+
* (deleted source, no session). Own component because the id list maps in a
|
|
295
|
+
* loop and the subscription is a hook.
|
|
296
|
+
*/
|
|
297
|
+
function DerivedFromLink({ session, id, first }: {
|
|
298
|
+
session: SemiontSession | null;
|
|
299
|
+
id: string;
|
|
300
|
+
first: boolean;
|
|
301
|
+
}) {
|
|
302
|
+
// `browse.resource` is stable per id (cache + scope wrappers memoize), so
|
|
303
|
+
// subscribing straight off the render read is safe; `failed` is an emission,
|
|
304
|
+
// never a stream error, so `readyValue` covers every unresolved state.
|
|
305
|
+
const descriptor = useObservable(session ? session.client.browse.resource(makeResourceId(id)) : null);
|
|
306
|
+
const name = descriptor ? readyValue(descriptor)?.name : undefined;
|
|
307
|
+
return (
|
|
308
|
+
<button
|
|
309
|
+
className="semiont-resource-info-panel__link"
|
|
310
|
+
onClick={() => session?.client.browse.openResource(makeResourceId(id))}
|
|
311
|
+
>
|
|
312
|
+
{!first && ', '}{name ?? id}
|
|
313
|
+
</button>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
@@ -6,7 +6,7 @@ import type { components, Selector } from '@semiont/core';
|
|
|
6
6
|
type JobProgress = components['schemas']['JobProgress'];
|
|
7
7
|
import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
|
|
8
8
|
import type { SemiontSession } from '@semiont/sdk';
|
|
9
|
-
import { annotatorKeyForMotivation, type Annotator } from '../../../lib/annotation-registry';
|
|
9
|
+
import { annotatorKeyForMotivation, type Annotator, type AnnotatorKey } from '../../../lib/annotation-registry';
|
|
10
10
|
import { StatisticsPanel } from './StatisticsPanel';
|
|
11
11
|
import { HighlightPanel } from './HighlightPanel';
|
|
12
12
|
import { ReferencesPanel } from './ReferencesPanel';
|
|
@@ -17,7 +17,9 @@ import './UnifiedAnnotationsPanel.css';
|
|
|
17
17
|
|
|
18
18
|
import type { Annotation } from '@semiont/core';
|
|
19
19
|
type Motivation = components['schemas']['Motivation'];
|
|
20
|
-
|
|
20
|
+
// Derived from the annotator registry (the single motivation↔annotator
|
|
21
|
+
// source) — a hand-written union here could drift from it.
|
|
22
|
+
type TabKey = 'statistics' | AnnotatorKey;
|
|
21
23
|
|
|
22
24
|
// Unified pending annotation type
|
|
23
25
|
interface PendingAnnotation {
|
|
@@ -76,6 +78,8 @@ interface UnifiedAnnotationsPanelProps {
|
|
|
76
78
|
entityTypesError?: Error | null;
|
|
77
79
|
referencedByError?: Error | null;
|
|
78
80
|
onRetryReferencedBy?: () => void;
|
|
81
|
+
/** Annotations currently sparkling (recently created or resolved) — RESOLUTION-SPARKLE D6. */
|
|
82
|
+
sparkleAnnotationIds?: Set<string>;
|
|
79
83
|
|
|
80
84
|
// Resource context — threaded to every per-motivation panel, which stamps it
|
|
81
85
|
// as `source` on mark:submit (multi-viewer routing).
|
|
@@ -328,6 +332,7 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
|
|
|
328
332
|
referencedByLoading={props.referencedByLoading}
|
|
329
333
|
referencedByError={props.referencedByError}
|
|
330
334
|
onRetryReferencedBy={props.onRetryReferencedBy}
|
|
335
|
+
sparkleAnnotationIds={props.sparkleAnnotationIds}
|
|
331
336
|
Link={props.Link}
|
|
332
337
|
routes={props.routes}
|
|
333
338
|
/>
|
|
@@ -94,6 +94,24 @@ describe('ReferenceEntry', () => {
|
|
|
94
94
|
mockGetEntityTypes.mockReturnValue([]);
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
// RESOLUTION-SPARKLE D6: a just-resolved reference announces itself on the
|
|
98
|
+
// panel entry's icon with the same `.annotation-sparkle` glow the document
|
|
99
|
+
// span uses. The host decides WHEN (membership in the sparkle set); the
|
|
100
|
+
// entry only renders the boolean.
|
|
101
|
+
describe('Resolution sparkle', () => {
|
|
102
|
+
it('carries the sparkle animation on the icon when sparkle is set', () => {
|
|
103
|
+
const { container } = renderEntry({ sparkle: true });
|
|
104
|
+
|
|
105
|
+
expect(container.querySelector('.semiont-reference-icon')).toHaveClass('annotation-sparkle');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('does not sparkle by default', () => {
|
|
109
|
+
const { container } = renderEntry();
|
|
110
|
+
|
|
111
|
+
expect(container.querySelector('.semiont-reference-icon')).not.toHaveClass('annotation-sparkle');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
97
115
|
describe('Rendering', () => {
|
|
98
116
|
it('should render the selected text in quotes', () => {
|
|
99
117
|
renderEntry();
|
|
@@ -172,6 +190,29 @@ describe('ReferenceEntry', () => {
|
|
|
172
190
|
|
|
173
191
|
expect(screen.getByText(/My Linked Document/)).toBeInTheDocument();
|
|
174
192
|
});
|
|
193
|
+
|
|
194
|
+
it('a resource-level derivation headlines Derived, never the generic type label', () => {
|
|
195
|
+
// The provenance edge minted by resource-focus generation: no selector
|
|
196
|
+
// (nothing to quote), resolved body naming the derived resource. The
|
|
197
|
+
// generic "Annotation" label said nothing — the qualifier says what
|
|
198
|
+
// the entry IS; the link line below says what it points at. Inference
|
|
199
|
+
// is from shape (the wire vocabulary deliberately has no 'deriving'
|
|
200
|
+
// purpose); the unresolved case keeps the plain type label.
|
|
201
|
+
mockGetAnnotationExactText.mockReturnValue('');
|
|
202
|
+
mockIsBodyResolved.mockReturnValue(true);
|
|
203
|
+
mockGetBodySource.mockReturnValue('gen-doc');
|
|
204
|
+
|
|
205
|
+
const provenanceRef = {
|
|
206
|
+
...createMockReference({ target: { source: 'resource-1' } }),
|
|
207
|
+
_resolvedDocumentName: 'generated from Cedar County, Iowa',
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
renderEntry({ reference: provenanceRef as Annotation });
|
|
211
|
+
|
|
212
|
+
expect(screen.getByText('ReferencesPanel.derived')).toBeInTheDocument();
|
|
213
|
+
expect(screen.queryByText('Annotation')).not.toBeInTheDocument();
|
|
214
|
+
expect(screen.getByText(/generated from Cedar County, Iowa/)).toBeInTheDocument();
|
|
215
|
+
});
|
|
175
216
|
});
|
|
176
217
|
|
|
177
218
|
describe('Entity types', () => {
|
|
@@ -4,7 +4,7 @@ import { render, screen, waitFor } from '@testing-library/react';
|
|
|
4
4
|
import userEvent from '@testing-library/user-event';
|
|
5
5
|
import '@testing-library/jest-dom';
|
|
6
6
|
import { ReferencesPanel } from '../ReferencesPanel';
|
|
7
|
-
import type { EventBus } from '@semiont/core';
|
|
7
|
+
import type { Annotation, AnnotationId, EventBus } from '@semiont/core';
|
|
8
8
|
import type { SemiontSession } from '@semiont/sdk';
|
|
9
9
|
import { createTestSemiontWrapper } from '../../../../test-utils';
|
|
10
10
|
|
|
@@ -118,6 +118,38 @@ describe('ReferencesPanel Component', () => {
|
|
|
118
118
|
vi.restoreAllMocks();
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
// RESOLUTION-SPARKLE D6: the panel threads the host's sparkle set down to
|
|
122
|
+
// the entries — exactly the ids in the set glow, nothing else.
|
|
123
|
+
describe('Resolution sparkle threading', () => {
|
|
124
|
+
const linkingReference = (id: string): Annotation => ({
|
|
125
|
+
'@context': 'http://www.w3.org/ns/anno.jsonld',
|
|
126
|
+
id: id as AnnotationId,
|
|
127
|
+
type: 'Annotation',
|
|
128
|
+
motivation: 'linking',
|
|
129
|
+
created: '2026-08-21T12:00:00Z',
|
|
130
|
+
modified: '2026-08-21T12:00:00Z',
|
|
131
|
+
target: {
|
|
132
|
+
source: 'res-1',
|
|
133
|
+
selector: { type: 'TextQuoteSelector', exact: `text of ${id}` },
|
|
134
|
+
},
|
|
135
|
+
body: { type: 'SpecificResource', source: 'linked-doc', purpose: 'linking' },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('sparkles exactly the entries named by sparkleAnnotationIds', () => {
|
|
139
|
+
const { container } = renderWithEventBus(
|
|
140
|
+
<ReferencesPanel
|
|
141
|
+
{...panelProps()}
|
|
142
|
+
annotations={[linkingReference('ann-dark'), linkingReference('ann-lit')]}
|
|
143
|
+
sparkleAnnotationIds={new Set(['ann-lit'])}
|
|
144
|
+
/>,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const sparkling = container.querySelectorAll('.semiont-reference-icon.annotation-sparkle');
|
|
148
|
+
expect(sparkling).toHaveLength(1);
|
|
149
|
+
expect(sparkling[0]!.closest('.semiont-annotation-entry')).toHaveTextContent('text of ann-lit');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
121
153
|
describe('Rendering', () => {
|
|
122
154
|
it('should render panel with title', () => {
|
|
123
155
|
renderWithEventBus(<ReferencesPanel {...panelProps()} />);
|
|
@@ -2,8 +2,10 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
4
4
|
import '@testing-library/jest-dom';
|
|
5
|
-
import
|
|
6
|
-
import type {
|
|
5
|
+
import { BehaviorSubject } from 'rxjs';
|
|
6
|
+
import type { EventBus, ResourceDescriptor } from '@semiont/core';
|
|
7
|
+
import { resourceId as makeResourceId } from '@semiont/core';
|
|
8
|
+
import type { CacheObservable, CacheState, SemiontClient, SemiontSession } from '@semiont/sdk';
|
|
7
9
|
import { ResourceInfoPanel } from '../ResourceInfoPanel';
|
|
8
10
|
import { createTestSemiontWrapper } from '../../../../test-utils';
|
|
9
11
|
|
|
@@ -281,17 +283,19 @@ describe('ResourceInfoPanel Component', () => {
|
|
|
281
283
|
});
|
|
282
284
|
|
|
283
285
|
describe('Generate Action', () => {
|
|
286
|
+
// The shell's collapsible header is ALSO a button named "Generate", so the
|
|
287
|
+
// form control is targeted by its exact accessible name (the ✨ is part of it).
|
|
284
288
|
it('renders the Generate button when onGenerate is provided', () => {
|
|
285
289
|
renderWithEventBus(
|
|
286
290
|
<ResourceInfoPanel {...defaultProps} onGenerate={() => {}} />
|
|
287
291
|
);
|
|
288
|
-
expect(screen.getByRole('button', { name:
|
|
292
|
+
expect(screen.getByRole('button', { name: '✨ Generate' })).toBeInTheDocument();
|
|
289
293
|
expect(screen.getByText("Generate a new resource from this one's context")).toBeInTheDocument();
|
|
290
294
|
});
|
|
291
295
|
|
|
292
296
|
it('hides the Generate button when onGenerate is omitted', () => {
|
|
293
297
|
renderWithEventBus(<ResourceInfoPanel {...defaultProps} />);
|
|
294
|
-
expect(screen.queryByRole('button', { name:
|
|
298
|
+
expect(screen.queryByRole('button', { name: '✨ Generate' })).not.toBeInTheDocument();
|
|
295
299
|
});
|
|
296
300
|
|
|
297
301
|
it('calls onGenerate when clicked', () => {
|
|
@@ -299,9 +303,117 @@ describe('ResourceInfoPanel Component', () => {
|
|
|
299
303
|
renderWithEventBus(
|
|
300
304
|
<ResourceInfoPanel {...defaultProps} onGenerate={onGenerate} />
|
|
301
305
|
);
|
|
302
|
-
fireEvent.click(screen.getByRole('button', { name:
|
|
306
|
+
fireEvent.click(screen.getByRole('button', { name: '✨ Generate' }));
|
|
303
307
|
expect(onGenerate).toHaveBeenCalledTimes(1);
|
|
304
308
|
});
|
|
309
|
+
|
|
310
|
+
// ── GENERATE-FROM-RESOURCE P2 (D7/D8): the panel is the progress surface ──
|
|
311
|
+
|
|
312
|
+
it('the Generate control lives inside the assist shell (D7)', () => {
|
|
313
|
+
const { container } = renderWithEventBus(
|
|
314
|
+
<ResourceInfoPanel {...defaultProps} onGenerate={() => {}}
|
|
315
|
+
isGenerating={false} generationProgress={null} />
|
|
316
|
+
);
|
|
317
|
+
const shell = container.querySelector('.semiont-assist-widget[data-type="generation"]');
|
|
318
|
+
expect(shell).not.toBeNull();
|
|
319
|
+
expect(shell!.querySelector('button')).not.toBeNull(); // the form IS the Generate control
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('progress replaces the Generate form while a generation runs', () => {
|
|
323
|
+
const { container } = renderWithEventBus(
|
|
324
|
+
<ResourceInfoPanel {...defaultProps} onGenerate={() => {}}
|
|
325
|
+
isGenerating
|
|
326
|
+
generationProgress={{ percentage: 40, message: { code: 'generating-resource' } }} />
|
|
327
|
+
);
|
|
328
|
+
expect(container.querySelector('.semiont-assist-progress')).not.toBeNull();
|
|
329
|
+
expect(screen.queryByRole('button', { name: '✨ Generate' })).toBeNull();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('the ended frame links the generated resource by name, and dismisses (D8)', () => {
|
|
333
|
+
const onDismissProgress = vi.fn();
|
|
334
|
+
const { client } = renderWithEventBus(
|
|
335
|
+
<ResourceInfoPanel {...defaultProps} onGenerate={() => {}}
|
|
336
|
+
isGenerating={false}
|
|
337
|
+
generationProgress={{ percentage: 100, message: { code: 'complete-generated', truncated: false } }}
|
|
338
|
+
generationOutcome={{
|
|
339
|
+
resourceId: makeResourceId('urn:semiont:resource:new1'),
|
|
340
|
+
resourceName: 'Summary of PB',
|
|
341
|
+
truncated: false,
|
|
342
|
+
}}
|
|
343
|
+
onDismissProgress={onDismissProgress} />
|
|
344
|
+
);
|
|
345
|
+
const openSpy = vi.spyOn(client.browse, 'openResource');
|
|
346
|
+
|
|
347
|
+
fireEvent.click(screen.getByRole('button', { name: 'Summary of PB' }));
|
|
348
|
+
expect(openSpy).toHaveBeenCalledTimes(1);
|
|
349
|
+
expect(String(openSpy.mock.calls[0]![0])).toBe('urn:semiont:resource:new1');
|
|
350
|
+
|
|
351
|
+
fireEvent.click(screen.getByTestId('semiont-assist-control'));
|
|
352
|
+
expect(onDismissProgress).toHaveBeenCalledTimes(1);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('a truncated completion says so; a natural stop does not (GFR P3b, D6)', () => {
|
|
356
|
+
// The mocked translator echoes unknown keys, so the assertion reads the
|
|
357
|
+
// KEY the real assistProgressCopy branch picked — structure, not copy.
|
|
358
|
+
const base = { ...defaultProps, onGenerate: () => {}, isGenerating: false };
|
|
359
|
+
const { unmount } = renderWithEventBus(
|
|
360
|
+
<ResourceInfoPanel {...base}
|
|
361
|
+
generationProgress={{ percentage: 100, message: { code: 'complete-generated', truncated: true } }} />
|
|
362
|
+
);
|
|
363
|
+
expect(screen.getByTestId('semiont-assist-status').textContent)
|
|
364
|
+
.toContain('codeCompleteGeneratedTruncated');
|
|
365
|
+
unmount();
|
|
366
|
+
|
|
367
|
+
renderWithEventBus(
|
|
368
|
+
<ResourceInfoPanel {...base}
|
|
369
|
+
generationProgress={{ percentage: 100, message: { code: 'complete-generated', truncated: false } }} />
|
|
370
|
+
);
|
|
371
|
+
expect(screen.getByTestId('semiont-assist-status').textContent)
|
|
372
|
+
.toContain('codeCompleteGenerated');
|
|
373
|
+
expect(screen.getByTestId('semiont-assist-status').textContent)
|
|
374
|
+
.not.toContain('Truncated');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('the ended frame speaks completion even when the final progress frame lost the race (GENERATION-ARRIVAL D4/D5)', () => {
|
|
378
|
+
// The screenshot case: last frame is the 95% "creating" payload, the
|
|
379
|
+
// terminal 100% frame never arrived — but the outcome did. The sentence
|
|
380
|
+
// derives from the OUTCOME, not the racing frame.
|
|
381
|
+
renderWithEventBus(
|
|
382
|
+
<ResourceInfoPanel {...defaultProps} onGenerate={() => {}}
|
|
383
|
+
isGenerating={false}
|
|
384
|
+
generationProgress={{ percentage: 95, message: { code: 'creating-resource' } }}
|
|
385
|
+
generationOutcome={{
|
|
386
|
+
resourceId: makeResourceId('urn:semiont:resource:new1'),
|
|
387
|
+
resourceName: 'Summary of PB',
|
|
388
|
+
truncated: false,
|
|
389
|
+
}} />
|
|
390
|
+
);
|
|
391
|
+
expect(screen.getByTestId('semiont-assist-status').textContent).toBe('codeCompleteGenerated');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('a truncated outcome says so — the bit rides the outcome, not the racing frame (D5)', () => {
|
|
395
|
+
renderWithEventBus(
|
|
396
|
+
<ResourceInfoPanel {...defaultProps} onGenerate={() => {}}
|
|
397
|
+
isGenerating={false}
|
|
398
|
+
generationProgress={{ percentage: 95, message: { code: 'creating-resource' } }}
|
|
399
|
+
generationOutcome={{
|
|
400
|
+
resourceId: makeResourceId('urn:semiont:resource:new1'),
|
|
401
|
+
resourceName: 'Summary of PB',
|
|
402
|
+
truncated: true,
|
|
403
|
+
}} />
|
|
404
|
+
);
|
|
405
|
+
expect(screen.getByTestId('semiont-assist-status').textContent).toBe('codeCompleteGeneratedTruncated');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('no outcome, no link: the ended frame renders without one until job:complete arrives', () => {
|
|
409
|
+
renderWithEventBus(
|
|
410
|
+
<ResourceInfoPanel {...defaultProps} onGenerate={() => {}}
|
|
411
|
+
isGenerating={false}
|
|
412
|
+
generationProgress={{ percentage: 100, message: { code: 'complete-generated', truncated: false } }}
|
|
413
|
+
generationOutcome={null} />
|
|
414
|
+
);
|
|
415
|
+
expect(screen.queryByText('Summary of PB')).toBeNull();
|
|
416
|
+
});
|
|
305
417
|
});
|
|
306
418
|
|
|
307
419
|
describe('Provenance Section', () => {
|
|
@@ -350,7 +462,9 @@ describe('ResourceInfoPanel Component', () => {
|
|
|
350
462
|
expect(screen.getByText('Alice, Bob')).toBeInTheDocument();
|
|
351
463
|
});
|
|
352
464
|
|
|
353
|
-
it('should render wasDerivedFrom when provided', () => {
|
|
465
|
+
it('should render wasDerivedFrom when provided — the id is the unresolved fallback', () => {
|
|
466
|
+
// The factory client's fetch fails in jsdom (no server), so the
|
|
467
|
+
// descriptor never resolves and the raw id stays as the honest label.
|
|
354
468
|
renderWithEventBus(
|
|
355
469
|
<ResourceInfoPanel
|
|
356
470
|
{...defaultProps}
|
|
@@ -382,6 +496,34 @@ describe('ResourceInfoPanel Component', () => {
|
|
|
382
496
|
expect(String(openSpy.mock.calls[0]![0])).toBe('urn:semiont:resource:abc123');
|
|
383
497
|
});
|
|
384
498
|
|
|
499
|
+
it('resolves the derived-from id to its resource NAME; clicks still navigate by id', () => {
|
|
500
|
+
// The id is wire identity, not presentation. Once `browse.resource`
|
|
501
|
+
// serves the source's descriptor, the link wears its name — the raw
|
|
502
|
+
// hash only survives while the descriptor is unresolved (pin below).
|
|
503
|
+
const { SemiontWrapper, session, client } = createTestSemiontWrapper();
|
|
504
|
+
vi.spyOn(client.browse, 'resource').mockReturnValue(
|
|
505
|
+
new BehaviorSubject<CacheState<ResourceDescriptor>>({
|
|
506
|
+
status: 'ready',
|
|
507
|
+
value: { '@id': 'urn:semiont:resource:abc123', name: 'Source Doc' } as ResourceDescriptor,
|
|
508
|
+
}) as unknown as CacheObservable<ResourceDescriptor>,
|
|
509
|
+
);
|
|
510
|
+
const openSpy = vi.spyOn(client.browse, 'openResource');
|
|
511
|
+
render(
|
|
512
|
+
<ResourceInfoPanel
|
|
513
|
+
{...defaultProps}
|
|
514
|
+
session={session}
|
|
515
|
+
wasDerivedFrom="urn:semiont:resource:abc123"
|
|
516
|
+
/>,
|
|
517
|
+
{ wrapper: ({ children }) => <SemiontWrapper>{children}</SemiontWrapper> },
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
expect(screen.getByText('Source Doc')).toBeInTheDocument();
|
|
521
|
+
expect(screen.queryByText('urn:semiont:resource:abc123')).toBeNull();
|
|
522
|
+
|
|
523
|
+
fireEvent.click(screen.getByText('Source Doc'));
|
|
524
|
+
expect(String(openSpy.mock.calls[0]![0])).toBe('urn:semiont:resource:abc123');
|
|
525
|
+
});
|
|
526
|
+
|
|
385
527
|
it('renders every id when the resource derives from several', () => {
|
|
386
528
|
renderWithEventBus(
|
|
387
529
|
<ResourceInfoPanel
|
|
@@ -28,10 +28,7 @@ vi.mock('../../../components/CodeMirrorRenderer', () => ({
|
|
|
28
28
|
const createMockTranslations = () => ({
|
|
29
29
|
title: 'Compose Resource',
|
|
30
30
|
titleEditClone: 'Edit Cloned Resource',
|
|
31
|
-
titleCompleteReference: 'Complete Reference',
|
|
32
31
|
subtitleClone: 'Editing a cloned resource',
|
|
33
|
-
subtitleReference: 'Creating a new resource for reference',
|
|
34
|
-
linkedNoticePrefix: 'This resource will be linked',
|
|
35
32
|
resourceName: 'Resource Name',
|
|
36
33
|
resourceNamePlaceholder: 'Enter resource name',
|
|
37
34
|
entityTypes: 'Entity Types',
|
|
@@ -53,9 +50,7 @@ const createMockTranslations = () => ({
|
|
|
53
50
|
cancel: 'Cancel',
|
|
54
51
|
saving: 'Saving...',
|
|
55
52
|
creating: 'Creating...',
|
|
56
|
-
creatingAndLinking: 'Creating and linking...',
|
|
57
53
|
saveClonedResource: 'Save Cloned Resource',
|
|
58
|
-
createAndLinkResource: 'Create and Link Resource',
|
|
59
54
|
createResource: 'Create Resource',
|
|
60
55
|
});
|
|
61
56
|
|
|
@@ -207,66 +202,6 @@ describe('ResourceComposePage', () => {
|
|
|
207
202
|
});
|
|
208
203
|
});
|
|
209
204
|
|
|
210
|
-
describe('Reference Completion Mode', () => {
|
|
211
|
-
it('shows reference completion title', () => {
|
|
212
|
-
const props = createMockProps({
|
|
213
|
-
mode: 'reference',
|
|
214
|
-
referenceData: {
|
|
215
|
-
annotationUri: 'http://localhost:8080/annotations/ref-1',
|
|
216
|
-
sourceDocumentId: 'doc-1',
|
|
217
|
-
name: 'Referenced Resource',
|
|
218
|
-
entityTypes: ['Document'],
|
|
219
|
-
},
|
|
220
|
-
});
|
|
221
|
-
renderWithProviders(<ResourceComposePage {...props} />);
|
|
222
|
-
|
|
223
|
-
expect(screen.getByText('Complete Reference')).toBeInTheDocument();
|
|
224
|
-
expect(screen.getByText('Creating a new resource for reference')).toBeInTheDocument();
|
|
225
|
-
expect(screen.getByText('This resource will be linked')).toBeInTheDocument();
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('initializes with reference data', () => {
|
|
229
|
-
const props = createMockProps({
|
|
230
|
-
mode: 'reference',
|
|
231
|
-
referenceData: {
|
|
232
|
-
annotationUri: 'http://localhost:8080/annotations/ref-1',
|
|
233
|
-
sourceDocumentId: 'doc-1',
|
|
234
|
-
name: 'Referenced Resource',
|
|
235
|
-
entityTypes: ['Document', 'Article'],
|
|
236
|
-
},
|
|
237
|
-
});
|
|
238
|
-
renderWithProviders(<ResourceComposePage {...props} />);
|
|
239
|
-
|
|
240
|
-
const nameInput = screen.getByLabelText('Resource Name') as HTMLInputElement;
|
|
241
|
-
expect(nameInput.value).toBe('Referenced Resource');
|
|
242
|
-
|
|
243
|
-
expect(screen.getByText('Document')).toBeInTheDocument();
|
|
244
|
-
expect(screen.getByText('Article')).toBeInTheDocument();
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('shows entity types as read-only when provided', () => {
|
|
248
|
-
const props = createMockProps({
|
|
249
|
-
mode: 'reference',
|
|
250
|
-
referenceData: {
|
|
251
|
-
annotationUri: 'http://localhost:8080/annotations/ref-1',
|
|
252
|
-
sourceDocumentId: 'doc-1',
|
|
253
|
-
name: 'Referenced Resource',
|
|
254
|
-
entityTypes: ['Document'],
|
|
255
|
-
},
|
|
256
|
-
});
|
|
257
|
-
renderWithProviders(<ResourceComposePage {...props} />);
|
|
258
|
-
|
|
259
|
-
// Should show read-only entity types
|
|
260
|
-
expect(screen.getByText('Document')).toBeInTheDocument();
|
|
261
|
-
|
|
262
|
-
// Should not show selectable buttons
|
|
263
|
-
expect(screen.queryByRole('button', { name: /Document entity type/ })).not.toBeInTheDocument();
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
// Entity Type Selection tests removed - functionality not present in current component
|
|
268
|
-
// The component only displays entity types in reference mode, doesn't have selection buttons
|
|
269
|
-
|
|
270
205
|
describe('Content Input Method', () => {
|
|
271
206
|
it('defaults to write mode', () => {
|
|
272
207
|
const props = createMockProps();
|