@semiont/react-ui 0.5.27 → 0.5.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/README.md +3 -3
  2. package/dist/{PdfAnnotationCanvas.client-EVUPTUWZ.js → PdfAnnotationCanvas.client-LSH3PZPY.js} +32 -5
  3. package/dist/{ar-OU3DQGC6.js → ar-AZBRADEB.js} +43 -23
  4. package/dist/{bn-PHWCR6AZ.js → bn-CCEW6AOS.js} +43 -23
  5. package/dist/{chunk-VXASXU4K.js → chunk-3ZBKCWUZ.js} +1 -1
  6. package/dist/{chunk-5VDTWRXJ.js → chunk-G4VHL4UA.js} +31 -31
  7. package/dist/{chunk-PCRRBU7H.js → chunk-SDKFZLEX.js} +43 -23
  8. package/dist/{cs-NEXYZ62I.js → cs-HXPF2CTE.js} +43 -23
  9. package/dist/{da-FAT43YCU.js → da-K5NTK7LH.js} +43 -23
  10. package/dist/{de-KRQ3WBCU.js → de-GMRQEDZI.js} +43 -23
  11. package/dist/{el-Q4IU3EKC.js → el-EUB4CI4G.js} +43 -23
  12. package/dist/{en-2KADE7N4.js → en-R5XA26NF.js} +2 -2
  13. package/dist/{es-2IO3R2MK.js → es-Z32DAZ6W.js} +43 -23
  14. package/dist/{fa-Q5GOQFXX.js → fa-G7HN6B32.js} +43 -23
  15. package/dist/{fi-R6NMRWBE.js → fi-3YBLZO7I.js} +43 -23
  16. package/dist/{fr-QY4D5E6J.js → fr-VUBH7BBS.js} +43 -23
  17. package/dist/{he-X6Y77XHY.js → he-WQQEY26B.js} +43 -23
  18. package/dist/{hi-XL7IOEQM.js → hi-HJZ6W4JB.js} +43 -23
  19. package/dist/{id-QZ4ZZS67.js → id-ZOGZ37MI.js} +43 -23
  20. package/dist/index.css +279 -74
  21. package/dist/index.d.ts +318 -393
  22. package/dist/index.js +2619 -2687
  23. package/dist/{it-ME2Q7OB6.js → it-DT6UW66F.js} +43 -23
  24. package/dist/{ja-HX7QASRI.js → ja-RUKQCOLO.js} +43 -23
  25. package/dist/{ko-EQO5AHAV.js → ko-ZH5ADKZM.js} +43 -23
  26. package/dist/{ms-UILFSF3I.js → ms-Y2AFPZTU.js} +43 -23
  27. package/dist/{nl-NNI45PMK.js → nl-OQEWIXNA.js} +43 -23
  28. package/dist/{no-MAL5F6P6.js → no-KPAYJZWY.js} +43 -23
  29. package/dist/{pl-4RMY6JAJ.js → pl-WSXK2UUD.js} +43 -23
  30. package/dist/{pt-VQH2X3QQ.js → pt-NWRJRUCK.js} +43 -23
  31. package/dist/{ro-XXCQO2OG.js → ro-3CGVMOC6.js} +43 -23
  32. package/dist/{sv-7Z6EJ6U4.js → sv-HGSEBA7S.js} +43 -23
  33. package/dist/test-utils.js +16 -7
  34. package/dist/{th-L3DXXYVQ.js → th-NZ4G352Z.js} +43 -23
  35. package/dist/{tr-P3DFVLFJ.js → tr-LGGS4DLW.js} +43 -23
  36. package/dist/{uk-7C5D3PXS.js → uk-AIX536L4.js} +43 -23
  37. package/dist/{vi-ZLTLEO7P.js → vi-V6CVQZLQ.js} +43 -23
  38. package/dist/{zh-5FHVWDIN.js → zh-AGKPGI3L.js} +43 -23
  39. package/package.json +16 -15
  40. package/src/components/AssistProgress.tsx +36 -2
  41. package/src/components/CodeMirrorRenderer.tsx +8 -8
  42. package/src/components/StatusDisplay.tsx +13 -13
  43. package/src/components/__tests__/AssistProgress.test.tsx +59 -0
  44. package/src/components/__tests__/StatusDisplay.test.tsx +9 -9
  45. package/src/components/modals/ComposeStep.tsx +233 -0
  46. package/src/components/modals/ConfigureGatherStep.tsx +10 -18
  47. package/src/components/modals/ConfigureGenerationStep.tsx +178 -34
  48. package/src/components/modals/ConfigureSearchStep.tsx +19 -0
  49. package/src/components/modals/ContextSummary.tsx +185 -72
  50. package/src/components/modals/CorpusPane.tsx +75 -0
  51. package/src/components/modals/DiscardPrompt.tsx +38 -0
  52. package/src/components/modals/GatherContextStep.tsx +237 -107
  53. package/src/components/modals/ReferenceWizardModal.tsx +252 -59
  54. package/src/components/modals/ResourceGenerateModal.tsx +225 -112
  55. package/src/components/modals/SearchModal.css +337 -101
  56. package/src/components/modals/SearchResultsStep.tsx +61 -68
  57. package/src/components/modals/WizardFooter.tsx +7 -1
  58. package/src/components/modals/__tests__/ComposeStep.test.tsx +152 -0
  59. package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +14 -26
  60. package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +206 -8
  61. package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +24 -1
  62. package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +463 -25
  63. package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +442 -39
  64. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +300 -86
  65. package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
  66. package/src/components/modals/__tests__/WizardFooter.test.tsx +116 -9
  67. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +34 -1
  68. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +38 -1
  69. package/src/components/resource/AnnotateView.tsx +3 -3
  70. package/src/components/resource/BrowseView.tsx +8 -8
  71. package/src/components/resource/DownloadFileLink.tsx +1 -1
  72. package/src/components/resource/ResourceViewer.tsx +4 -4
  73. package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +1 -1
  74. package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +1 -1
  75. package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +1 -1
  76. package/src/components/resource/__tests__/BrowseView.test.tsx +11 -11
  77. package/src/components/resource/__tests__/download-link.test.tsx +6 -6
  78. package/src/components/resource/annotate-renderers.tsx +3 -3
  79. package/src/components/resource/panels/AssistSection.css +35 -0
  80. package/src/components/resource/panels/ReferenceEntry.tsx +11 -4
  81. package/src/components/resource/panels/ReferencesPanel.tsx +9 -1
  82. package/src/components/resource/panels/ResourceInfoPanel.tsx +89 -20
  83. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +7 -2
  84. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +41 -0
  85. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +1 -1
  86. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +84 -3
  87. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +149 -7
  88. package/src/features/auth/__tests__/SignInForm.a11y.test.tsx +12 -12
  89. package/src/features/auth/components/SignInForm.tsx +53 -53
  90. package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +2 -2
  91. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +0 -65
  92. package/src/features/resource-compose/components/ResourceComposePage.tsx +35 -133
  93. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +1 -100
  94. package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -57
  95. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +1 -1
  96. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +2 -2
  97. package/src/features/resource-discovery/state/discover-state-unit.ts +1 -1
  98. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +73 -17
  99. package/src/features/resource-viewer/__tests__/generation-options.test.ts +53 -0
  100. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +148 -83
  101. package/src/features/resource-viewer/generation-options.ts +29 -0
  102. package/src/styles/core/forms.css +35 -8
  103. package/src/styles/features/resource-viewer.css +0 -22
  104. package/src/styles/features/resource.css +0 -4
  105. package/src/styles/index.css +0 -1
  106. package/translations/ar.json +42 -22
  107. package/translations/bn.json +42 -22
  108. package/translations/cs.json +42 -22
  109. package/translations/da.json +42 -22
  110. package/translations/de.json +42 -22
  111. package/translations/el.json +42 -22
  112. package/translations/en.json +42 -22
  113. package/translations/es.json +42 -22
  114. package/translations/fa.json +42 -22
  115. package/translations/fi.json +42 -22
  116. package/translations/fr.json +42 -22
  117. package/translations/he.json +42 -22
  118. package/translations/hi.json +42 -22
  119. package/translations/id.json +42 -22
  120. package/translations/it.json +42 -22
  121. package/translations/ja.json +42 -22
  122. package/translations/ko.json +42 -22
  123. package/translations/ms.json +42 -22
  124. package/translations/nl.json +42 -22
  125. package/translations/no.json +42 -22
  126. package/translations/pl.json +42 -22
  127. package/translations/pt.json +42 -22
  128. package/translations/ro.json +42 -22
  129. package/translations/sv.json +42 -22
  130. package/translations/th.json +42 -22
  131. package/translations/tr.json +42 -22
  132. package/translations/uk.json +42 -22
  133. package/translations/vi.json +42 -22
  134. package/translations/zh.json +42 -22
  135. package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js.map +0 -1
  136. package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css.map +0 -1
  137. package/dist/ar-OU3DQGC6.js.map +0 -1
  138. package/dist/bn-PHWCR6AZ.js.map +0 -1
  139. package/dist/chunk-5VDTWRXJ.js.map +0 -1
  140. package/dist/chunk-BTUG3VJJ.js.map +0 -1
  141. package/dist/chunk-O23TLLXW.js.map +0 -1
  142. package/dist/chunk-PCRRBU7H.js.map +0 -1
  143. package/dist/chunk-QT7LYM72.js.map +0 -1
  144. package/dist/chunk-VXASXU4K.js.map +0 -1
  145. package/dist/cs-NEXYZ62I.js.map +0 -1
  146. package/dist/da-FAT43YCU.js.map +0 -1
  147. package/dist/de-KRQ3WBCU.js.map +0 -1
  148. package/dist/el-Q4IU3EKC.js.map +0 -1
  149. package/dist/en-2KADE7N4.js.map +0 -1
  150. package/dist/es-2IO3R2MK.js.map +0 -1
  151. package/dist/fa-Q5GOQFXX.js.map +0 -1
  152. package/dist/fi-R6NMRWBE.js.map +0 -1
  153. package/dist/fr-QY4D5E6J.js.map +0 -1
  154. package/dist/he-X6Y77XHY.js.map +0 -1
  155. package/dist/hi-XL7IOEQM.js.map +0 -1
  156. package/dist/id-QZ4ZZS67.js.map +0 -1
  157. package/dist/index.css.map +0 -1
  158. package/dist/index.js.map +0 -1
  159. package/dist/integrations/css-modules-helper.js.map +0 -1
  160. package/dist/integrations/styled-components-theme.js.map +0 -1
  161. package/dist/it-ME2Q7OB6.js.map +0 -1
  162. package/dist/ja-HX7QASRI.js.map +0 -1
  163. package/dist/ko-EQO5AHAV.js.map +0 -1
  164. package/dist/ms-UILFSF3I.js.map +0 -1
  165. package/dist/nl-NNI45PMK.js.map +0 -1
  166. package/dist/no-MAL5F6P6.js.map +0 -1
  167. package/dist/pl-4RMY6JAJ.js.map +0 -1
  168. package/dist/pt-VQH2X3QQ.js.map +0 -1
  169. package/dist/ro-XXCQO2OG.js.map +0 -1
  170. package/dist/sv-7Z6EJ6U4.js.map +0 -1
  171. package/dist/test-utils.css.map +0 -1
  172. package/dist/test-utils.js.map +0 -1
  173. package/dist/th-L3DXXYVQ.js.map +0 -1
  174. package/dist/tr-P3DFVLFJ.js.map +0 -1
  175. package/dist/uk-7C5D3PXS.js.map +0 -1
  176. package/dist/vi-ZLTLEO7P.js.map +0 -1
  177. package/dist/zh-5FHVWDIN.js.map +0 -1
  178. package/src/components/modals/ResourceSearchModal.tsx +0 -224
  179. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +0 -163
  180. package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +0 -137
  181. package/src/features/admin-exchange/__tests__/ExportCard.test.tsx +0 -41
  182. package/src/features/admin-exchange/__tests__/ImportCard.test.tsx +0 -148
  183. package/src/features/admin-exchange/__tests__/ImportProgress.test.tsx +0 -117
  184. package/src/features/admin-exchange/components/AdminExchangePage.tsx +0 -117
  185. package/src/features/admin-exchange/components/ExportCard.tsx +0 -35
  186. package/src/features/admin-exchange/components/ImportCard.tsx +0 -183
  187. package/src/features/admin-exchange/components/ImportProgress.tsx +0 -80
  188. package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +0 -185
  189. package/src/features/admin-exchange/state/exchange-state-unit.ts +0 -133
  190. package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +0 -118
  191. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +0 -118
  192. package/src/styles/features/exchange.css +0 -374
@@ -173,6 +173,41 @@
173
173
  font-variant-numeric: tabular-nums;
174
174
  }
175
175
 
176
+ /* D8: the finished run's artifact, offered as a link. */
177
+ /* The finished run's artifact — the ended frame's primary affordance
178
+ (GENERATION-ARRIVAL P1, D3's fallback): a button-shaped invitation to open
179
+ what was made, not an underlined footnote beside the bar. */
180
+ .semiont-assist-progress__outcome {
181
+ align-self: stretch;
182
+ padding: 0.5rem 0.75rem;
183
+ border: 1px solid var(--semiont-color-primary-300);
184
+ border-radius: 0.5rem;
185
+ background: var(--semiont-color-primary-50);
186
+ cursor: pointer;
187
+ font-size: var(--semiont-text-sm);
188
+ font-weight: var(--semiont-font-medium);
189
+ color: var(--semiont-color-primary-700);
190
+ text-align: left;
191
+ }
192
+
193
+ .semiont-assist-progress__outcome:hover {
194
+ border-color: var(--semiont-color-primary-400);
195
+ background: var(--semiont-color-primary-100);
196
+ color: var(--semiont-color-primary-800);
197
+ }
198
+
199
+ [data-theme="dark"] .semiont-assist-progress__outcome {
200
+ border-color: var(--semiont-color-primary-700);
201
+ background: var(--semiont-color-primary-900);
202
+ color: var(--semiont-color-primary-300);
203
+ }
204
+
205
+ [data-theme="dark"] .semiont-assist-progress__outcome:hover {
206
+ border-color: var(--semiont-color-primary-600);
207
+ background: var(--semiont-color-primary-800);
208
+ color: var(--semiont-color-primary-200);
209
+ }
210
+
176
211
  .semiont-assist-progress__params {
177
212
  display: flex;
178
213
  flex-wrap: wrap;
@@ -11,7 +11,7 @@ import type { SemiontSession } from '@semiont/sdk';
11
11
  import { renderAgentLabel } from './agent-label';
12
12
  import { useHoverEmitter } from '../../../hooks/useHoverEmitter';
13
13
 
14
- // Extended annotation type with runtime properties added by backend enrichment
14
+ // Extended annotation type with runtime properties added by gateway enrichment
15
15
  interface EnrichedAnnotation extends Annotation {
16
16
  _resolvedDocumentName?: string;
17
17
  _resolvedDocumentMediaType?: string;
@@ -27,6 +27,8 @@ interface ReferenceEntryProps {
27
27
  onOpenResource?: (resourceId: string) => void;
28
28
  annotateMode?: boolean;
29
29
  isGenerating?: boolean;
30
+ /** The reference just got attention-worthy (created or resolved) — glow the icon (RESOLUTION-SPARKLE D6). */
31
+ sparkle?: boolean;
30
32
  ref?: Ref<HTMLDivElement>;
31
33
  }
32
34
 
@@ -38,6 +40,7 @@ export function ReferenceEntry({
38
40
  onOpenResource,
39
41
  annotateMode = true,
40
42
  isGenerating = false,
43
+ sparkle = false,
41
44
  ref,
42
45
  }: ReferenceEntryProps) {
43
46
  const t = useTranslations('ReferencesPanel');
@@ -55,7 +58,7 @@ export function ReferenceEntry({
55
58
  const hasSvgSelector = getSvgSelector(selector);
56
59
  const annotationType = hasFragmentSelector ? 'Fragment annotation' : hasSvgSelector ? 'Image annotation' : 'Annotation';
57
60
 
58
- // Extract resolved document name and media type if enriched by backend
61
+ // Extract resolved document name and media type if enriched by gateway
59
62
  const enrichedReference = reference as EnrichedAnnotation;
60
63
  const resolvedDocumentName = enrichedReference._resolvedDocumentName;
61
64
  const resolvedDocumentMediaType = enrichedReference._resolvedDocumentMediaType;
@@ -125,7 +128,7 @@ export function ReferenceEntry({
125
128
  <div className="semiont-annotation-entry__header">
126
129
  <div className="semiont-reference-icon-group">
127
130
  <button
128
- className={`semiont-reference-icon${iconIsClickable ? ' semiont-reference-icon--clickable' : ''}`}
131
+ className={`semiont-reference-icon${iconIsClickable ? ' semiont-reference-icon--clickable' : ''}${sparkle ? ' annotation-sparkle' : ''}`}
129
132
  title={isResolved ? t('open') : annotateMode ? t('resolve') : t('stub')}
130
133
  onClick={iconIsClickable ? handleIconClick : undefined}
131
134
  data-generating={!isResolved && isGenerating ? 'true' : 'false'}
@@ -151,7 +154,11 @@ export function ReferenceEntry({
151
154
  )}
152
155
  {!selectedText && (
153
156
  <div className="semiont-annotation-entry__meta">
154
- {annotationType}
157
+ {/* A resource-level annotation with a resolved, named target is
158
+ a derivation (the shape generation mints — the vocabulary
159
+ deliberately has no 'deriving' purpose): qualify the link
160
+ line below instead of showing the generic type label. */}
161
+ {!selector && resolvedDocumentName ? t('derived') : annotationType}
155
162
  </div>
156
163
  )}
157
164
  {resolvedDocumentName && (
@@ -83,6 +83,8 @@ interface Props {
83
83
  scrollToAnnotationId?: string | null;
84
84
  onScrollCompleted?: () => void;
85
85
  hoveredAnnotationId?: string | null;
86
+ /** Annotations currently sparkling (recently created or resolved) — entries in the set glow their icon (RESOLUTION-SPARKLE D6). */
87
+ sparkleAnnotationIds?: Set<string>;
86
88
 
87
89
  /** User UI locale — stamped on the unresolved-reference body's `language` field. */
88
90
  locale?: string;
@@ -119,6 +121,7 @@ export function ReferencesPanel({
119
121
  scrollToAnnotationId,
120
122
  onScrollCompleted,
121
123
  hoveredAnnotationId,
124
+ sparkleAnnotationIds,
122
125
  locale,
123
126
  sourceLanguage,
124
127
  }: Props) {
@@ -276,7 +279,11 @@ export function ReferencesPanel({
276
279
  source: resourceId,
277
280
  motivation: 'linking',
278
281
  selector: pendingAnnotation.selector,
279
- body: entityType ? [{ type: 'TextualBody', value: entityType, purpose: 'tagging' }] : [],
282
+ // Entity types are optional, and MarkSubmitEvent.body is minItems:1
283
+ // an empty array is a 400, not an empty annotation.
284
+ ...(entityType
285
+ ? { body: [{ type: 'TextualBody' as const, value: entityType, purpose: 'tagging' as const }] }
286
+ : {}),
280
287
  });
281
288
  setPendingEntityTypes([]);
282
289
  }
@@ -483,6 +490,7 @@ export function ReferencesPanel({
483
490
  onOpenResource={onOpenResource}
484
491
  annotateMode={annotateMode}
485
492
  isGenerating={reference.id === generatingReferenceId}
493
+ sparkle={sparkleAnnotationIds?.has(reference.id) ?? false}
486
494
  ref={(el) => setEntryRef(reference.id, el)}
487
495
  />
488
496
  ))
@@ -1,13 +1,17 @@
1
1
  'use client';
2
2
 
3
3
  import { useTranslations } from '../../../contexts/TranslationContext';
4
- import type { SemiontSession } from '@semiont/sdk';
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
- <button
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 Action opens the resource-generate flow (UI-only) */}
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
- <div className="semiont-resource-info-panel__action-section">
191
- <button
192
- onClick={onGenerate}
193
- className="semiont-resource-button semiont-resource-button--secondary"
194
- >
195
- {t('generate')}
196
- </button>
197
- <p className="semiont-resource-info-panel__description">
198
- {t('generateDescription')}
199
- </p>
200
- </div>
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
- type TabKey = 'statistics' | 'reference' | 'highlight' | 'assessment' | 'comment' | 'tag';
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', () => {
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Written after an e2e failure (test 05) where `ReferencesPanel` rendered
5
5
  * "No entity types available" even though the client provably received a
6
- * 9-string entity-types array from the backend.
6
+ * 9-string entity-types array from the gateway.
7
7
  *
8
8
  * This test closes the Layer 5-6 gap: the existing `ResourceViewerPage.test.tsx`
9
9
  * stubs `UnifiedAnnotationsPanel`/`ReferencesPanel` as `<div data-testid>`s,
@@ -1,10 +1,10 @@
1
1
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import React from 'react';
3
- import { render, screen, waitFor } from '@testing-library/react';
3
+ import { render, screen, waitFor, within } 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
 
@@ -20,7 +20,7 @@ function createEventTracker() {
20
20
  events,
21
21
  clear: () => { events.length = 0; },
22
22
  _attach(eventBus: EventBus) {
23
- const panelEvents = ['mark:assist-request'] as const;
23
+ const panelEvents = ['mark:assist-request', 'mark:submit'] as const;
24
24
  panelEvents.forEach((eventName) => {
25
25
  eventBus.get(eventName).subscribe((payload: any) => {
26
26
  events.push({ event: eventName, payload });
@@ -66,6 +66,7 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
66
66
  includeDescriptiveReferences: 'Include descriptive references',
67
67
  descriptiveReferencesTooltip: 'Also find phrases like \'the CEO\', \'the tech giant\', \'the physicist\' (in addition to names)',
68
68
  cancel: 'Cancel',
69
+ createReference: 'Create Reference',
69
70
  annotating: 'Annotating...',
70
71
  // CLEAN-PROGRESS D3: the widget's own strings come from the
71
72
  // AssistProgress namespace now, not from this panel's.
@@ -118,6 +119,38 @@ describe('ReferencesPanel Component', () => {
118
119
  vi.restoreAllMocks();
119
120
  });
120
121
 
122
+ // RESOLUTION-SPARKLE D6: the panel threads the host's sparkle set down to
123
+ // the entries — exactly the ids in the set glow, nothing else.
124
+ describe('Resolution sparkle threading', () => {
125
+ const linkingReference = (id: string): Annotation => ({
126
+ '@context': 'http://www.w3.org/ns/anno.jsonld',
127
+ id: id as AnnotationId,
128
+ type: 'Annotation',
129
+ motivation: 'linking',
130
+ created: '2026-08-21T12:00:00Z',
131
+ modified: '2026-08-21T12:00:00Z',
132
+ target: {
133
+ source: 'res-1',
134
+ selector: { type: 'TextQuoteSelector', exact: `text of ${id}` },
135
+ },
136
+ body: { type: 'SpecificResource', source: 'linked-doc', purpose: 'linking' },
137
+ });
138
+
139
+ it('sparkles exactly the entries named by sparkleAnnotationIds', () => {
140
+ const { container } = renderWithEventBus(
141
+ <ReferencesPanel
142
+ {...panelProps()}
143
+ annotations={[linkingReference('ann-dark'), linkingReference('ann-lit')]}
144
+ sparkleAnnotationIds={new Set(['ann-lit'])}
145
+ />,
146
+ );
147
+
148
+ const sparkling = container.querySelectorAll('.semiont-reference-icon.annotation-sparkle');
149
+ expect(sparkling).toHaveLength(1);
150
+ expect(sparkling[0]!.closest('.semiont-annotation-entry')).toHaveTextContent('text of ann-lit');
151
+ });
152
+ });
153
+
121
154
  describe('Rendering', () => {
122
155
  it('should render panel with title', () => {
123
156
  renderWithEventBus(<ReferencesPanel {...panelProps()} />);
@@ -147,6 +180,54 @@ describe('ReferencesPanel Component', () => {
147
180
  });
148
181
  });
149
182
 
183
+ // Entity types are optional on a reference, but `MarkSubmitEvent.body` is
184
+ // `minItems: 1` — so "no types selected" must OMIT body, not send `[]`.
185
+ // Sending `[]` 400s at /bus/emit, and `mark.submit` is fire-and-forget, so
186
+ // the failure is silent: the button looks inert. Found live 2026-08-24.
187
+ describe('Create Reference payload', () => {
188
+ const pendingLinking = {
189
+ motivation: 'linking' as const,
190
+ selector: { type: 'TextQuoteSelector' as const, exact: 'thylakoid' },
191
+ };
192
+
193
+ const submitPayloads = (tracker: ReturnType<typeof createEventTracker>) =>
194
+ tracker.events.filter(e => e.event === 'mark:submit').map(e => e.payload);
195
+
196
+ // The panel's own entity-type chips, not AssistSection's identically
197
+ // labelled ones.
198
+ const renderPrompt = (tracker: ReturnType<typeof createEventTracker>) => {
199
+ const { container } = renderWithEventBus(
200
+ <ReferencesPanel {...panelProps()} pendingAnnotation={pendingLinking} />,
201
+ tracker,
202
+ );
203
+ return within(container.querySelector('.semiont-annotation-prompt') as HTMLElement);
204
+ };
205
+
206
+ it('omits body entirely when no entity type is selected', async () => {
207
+ const tracker = createEventTracker();
208
+ const prompt = renderPrompt(tracker);
209
+
210
+ await userEvent.click(prompt.getByRole('button', { name: /create reference/i }));
211
+
212
+ const [payload] = submitPayloads(tracker);
213
+ expect(payload).toBeDefined();
214
+ expect(payload).not.toHaveProperty('body');
215
+ });
216
+
217
+ it('sends a one-item body when an entity type is selected', async () => {
218
+ const tracker = createEventTracker();
219
+ const prompt = renderPrompt(tracker);
220
+
221
+ await userEvent.click(prompt.getByRole('button', { name: 'Person' }));
222
+ await userEvent.click(prompt.getByRole('button', { name: /create reference/i }));
223
+
224
+ const [payload] = submitPayloads(tracker);
225
+ expect(payload.body).toEqual([
226
+ { type: 'TextualBody', value: 'Person', purpose: 'tagging' },
227
+ ]);
228
+ });
229
+ });
230
+
150
231
  describe('Entity Type Selection', () => {
151
232
  it('should toggle entity type selection on click', async () => {
152
233
  renderWithEventBus(<ReferencesPanel {...panelProps()} />);