@semiont/react-ui 0.5.26 → 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/README.md +28 -4
- package/dist/{PdfAnnotationCanvas.client-6ZSF6ZTJ.js → PdfAnnotationCanvas.client-C2IGG4SW.js} +33 -6
- package/dist/{PdfAnnotationCanvas.client-JCJMXWWJ.css → PdfAnnotationCanvas.client-TGDPPGXJ.css} +6 -1
- package/dist/{ar-YTUUGK2O.js → ar-FABPTJVB.js} +88 -26
- package/dist/{bn-ASGNGPMD.js → bn-LKP6BDMH.js} +88 -26
- package/dist/{chunk-L3WKRWP3.js → chunk-2GOHTTHW.js} +88 -26
- package/dist/{chunk-OBQOZCJE.js → chunk-2YHEE63S.js} +31 -31
- package/dist/{cs-WMYVAOZY.js → cs-YQYNWHRG.js} +88 -26
- package/dist/{da-FSHHN4D6.js → da-QHZOYKRY.js} +88 -26
- package/dist/{de-FJFO3TPT.js → de-GKA7RD7J.js} +88 -26
- package/dist/{el-XOH4QIAG.js → el-45T257AL.js} +88 -26
- package/dist/{en-3FCBW3YD.js → en-RVW5EQ66.js} +4 -2
- package/dist/{es-HDX4QI7E.js → es-AVER7RU4.js} +88 -26
- package/dist/{fa-DJ6WTFTJ.js → fa-K3VPK3O2.js} +88 -26
- package/dist/{fi-4S7H7W7U.js → fi-5NEKF7EB.js} +88 -26
- package/dist/{fr-6UJSYHXC.js → fr-FUMNDLBZ.js} +88 -26
- package/dist/{he-ZZJOHJX2.js → he-3JGNA3PR.js} +88 -26
- package/dist/{hi-HZJTAW7U.js → hi-IJY375DF.js} +88 -26
- package/dist/{id-E3I6ILEM.js → id-LWHKJ7ZD.js} +88 -26
- package/dist/index.css +380 -524
- package/dist/index.d.ts +318 -192
- package/dist/index.js +2960 -2421
- package/dist/integrations/css-modules-helper.js +1 -3
- package/dist/{it-ULAPAD7U.js → it-EBVOV4RW.js} +88 -26
- package/dist/{ja-SAHZQ4CZ.js → ja-KWRIX2NF.js} +88 -26
- package/dist/{ko-DFU2ADQ4.js → ko-L5CJMFDS.js} +88 -26
- package/dist/{ms-NWL76XNT.js → ms-LSNMJDR2.js} +88 -26
- package/dist/{nl-ATCM76WN.js → nl-ZB32MB7U.js} +88 -26
- package/dist/{no-UIAQ5RJF.js → no-4GTNY7M5.js} +88 -26
- package/dist/{pl-NOECQDXD.js → pl-LUHBLTHB.js} +88 -26
- package/dist/{pt-SVWIIUPC.js → pt-C4H3UNQ6.js} +88 -26
- package/dist/{ro-NR2DRTZG.js → ro-GGITS5V4.js} +88 -26
- package/dist/{sv-STVJ37JP.js → sv-T56RTLBP.js} +88 -26
- package/dist/test-utils.css +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/dist/test-utils.js +2 -2
- package/dist/{th-UWQR4K54.js → th-22X2IWCB.js} +88 -26
- package/dist/{tr-WNXZSD27.js → tr-77TFG62H.js} +88 -26
- package/dist/{uk-SJRMB47Y.js → uk-MIIL6NXT.js} +88 -26
- package/dist/{vi-TMRQFES2.js → vi-7JYOIGNV.js} +88 -26
- package/dist/{zh-QOL3LYEM.js → zh-ID6DRYU4.js} +88 -26
- package/package.json +11 -10
- package/src/components/AssistProgress.tsx +178 -134
- package/src/components/CodeMirrorRenderer.tsx +8 -8
- package/src/components/{EntityFoundLog.tsx → ItemFoundLog.tsx} +12 -7
- package/src/components/ProtectedErrorBoundary.css +3 -1
- package/src/components/StatusDisplay.css +1 -1
- package/src/components/Toast.css +3 -1
- package/src/components/__tests__/AssistProgress.test.tsx +268 -141
- package/src/components/annotation/annotation-entries.css +0 -4
- package/src/components/annotation/references.css +0 -4
- package/src/components/image-annotation/AnnotationOverlay.tsx +6 -6
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +1 -1
- package/src/components/image-annotation/__tests__/AnnotationOverlay.click.test.tsx +7 -9
- package/src/components/image-annotation/__tests__/SvgDrawingCanvas.click.test.tsx +1 -2
- package/src/components/layout/Header.css +0 -4
- package/src/components/layout/LeftSidebar.css +0 -4
- package/src/components/modals/ComposeStep.tsx +218 -0
- package/src/components/modals/ConfigureGatherStep.tsx +10 -18
- package/src/components/modals/ConfigureGenerationStep.tsx +131 -54
- package/src/components/modals/ConfigureSearchStep.tsx +42 -40
- 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 +287 -60
- package/src/components/modals/ResourceGenerateModal.tsx +233 -109
- package/src/components/modals/SearchModal.css +335 -111
- package/src/components/modals/SearchModal.tsx +1 -1
- package/src/components/modals/SearchResultsStep.tsx +63 -88
- package/src/components/modals/WizardFooter.tsx +69 -0
- 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 +290 -0
- package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +95 -0
- package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +463 -25
- package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +635 -0
- package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +277 -85
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +6 -2
- package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
- package/src/components/modals/__tests__/WizardFooter.test.tsx +344 -0
- package/src/components/modals/modals.css +16 -42
- package/src/components/navigation/NavigationTabs.css +0 -20
- package/src/components/pdf-annotation/PdfAnnotationCanvas.css +21 -2
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +36 -3
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +41 -6
- package/src/components/resource/AnnotateView.tsx +19 -8
- package/src/components/resource/AnnotationHistory.tsx +2 -0
- package/src/components/resource/BrowseView.tsx +20 -9
- package/src/components/resource/ResourceViewer.tsx +27 -28
- package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +1 -1
- package/src/components/resource/__tests__/AnnotateView.focus.test.tsx +50 -0
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +2 -0
- package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +1 -1
- package/src/components/resource/__tests__/BrowseView.test.tsx +38 -12
- package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +47 -3
- package/src/components/resource/annotate-renderers.tsx +3 -3
- package/src/components/resource/event-formatting.ts +0 -3
- package/src/components/resource/panels/AssessmentEntry.tsx +1 -1
- package/src/components/resource/panels/AssessmentPanel.css +5 -6
- package/src/components/resource/panels/AssistSection.css +95 -263
- package/src/components/resource/panels/AssistSection.tsx +4 -4
- package/src/components/resource/panels/AssistShell.tsx +12 -4
- package/src/components/resource/panels/CollaborationPanel.css +47 -22
- package/src/components/resource/panels/CollaborationPanel.tsx +93 -2
- package/src/components/resource/panels/CommentEntry.tsx +1 -1
- package/src/components/resource/panels/CommentsPanel.css +0 -16
- package/src/components/resource/panels/HighlightEntry.tsx +1 -1
- package/src/components/resource/panels/HighlightPanel.css +0 -7
- package/src/components/resource/panels/JsonLdPanel.css +0 -4
- package/src/components/resource/panels/ReferenceEntry.tsx +10 -3
- package/src/components/resource/panels/ReferencesPanel.css +0 -12
- package/src/components/resource/panels/ReferencesPanel.tsx +16 -20
- package/src/components/resource/panels/ResourceInfoPanel.css +0 -32
- package/src/components/resource/panels/ResourceInfoPanel.tsx +89 -20
- package/src/components/resource/panels/StatisticsPanel.css +0 -20
- package/src/components/resource/panels/TagEntry.tsx +1 -1
- package/src/components/resource/panels/TaggingPanel.css +0 -4
- package/src/components/resource/panels/TaggingPanel.tsx +2 -10
- package/src/components/resource/panels/UnifiedAnnotationsPanel.css +0 -4
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +7 -2
- package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/AssistSection.test.tsx +48 -50
- package/src/components/resource/panels/__tests__/AssistShell.test.tsx +11 -6
- package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +89 -0
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +1 -2
- package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +44 -36
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +41 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +1 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +67 -54
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +185 -7
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +0 -1
- package/src/components/settings/SettingsPanel.css +0 -4
- package/src/features/auth/auth.css +2 -26
- 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 -76
- package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -48
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +55 -0
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +29 -0
- package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +53 -3
- package/src/features/resource-discovery/state/discover-state-unit.ts +29 -8
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +71 -15
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +166 -71
- package/src/features/resource-viewer/hooks/__tests__/useResourceViewedReport.test.tsx +63 -0
- package/src/features/resource-viewer/hooks/useResourceViewedReport.ts +33 -0
- package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +2 -2
- package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +1 -1
- package/src/styles/core/buttons.css +3 -1
- package/src/styles/core/forms.css +35 -29
- package/src/styles/core/indicators.css +0 -4
- package/src/styles/core/inputs.css +0 -4
- package/src/styles/core/progress.css +2 -34
- package/src/styles/core/sliders.css +0 -4
- package/src/styles/core/toggles.css +0 -4
- package/src/styles/features/resource-discovery.css +13 -0
- package/src/styles/features/resource-viewer.css +0 -42
- package/src/styles/features/resource.css +0 -4
- package/src/styles/motivations/motivation-assessment.css +2 -2
- package/src/styles/panels/history-panel.css +0 -40
- package/src/styles/panels/user-panel.css +0 -3
- package/src/styles/patterns/panel-helpers.css +0 -27
- package/src/styles/patterns/panel-sections.css +0 -59
- package/src/styles/patterns/panels-base.css +0 -32
- package/translations/ar.json +84 -24
- package/translations/bn.json +84 -24
- package/translations/cs.json +84 -24
- package/translations/da.json +84 -24
- package/translations/de.json +84 -24
- package/translations/el.json +84 -24
- package/translations/en.json +84 -24
- package/translations/es.json +84 -24
- package/translations/fa.json +84 -24
- package/translations/fi.json +84 -24
- package/translations/fr.json +84 -24
- package/translations/he.json +84 -24
- package/translations/hi.json +84 -24
- package/translations/id.json +84 -24
- package/translations/it.json +84 -24
- package/translations/ja.json +84 -24
- package/translations/ko.json +84 -24
- package/translations/ms.json +84 -24
- package/translations/nl.json +84 -24
- package/translations/no.json +84 -24
- package/translations/pl.json +84 -24
- package/translations/pt.json +84 -24
- package/translations/ro.json +84 -24
- package/translations/sv.json +84 -24
- package/translations/th.json +84 -24
- package/translations/tr.json +84 -24
- package/translations/uk.json +84 -24
- package/translations/vi.json +84 -24
- package/translations/zh.json +84 -24
- package/dist/PdfAnnotationCanvas.client-6ZSF6ZTJ.js.map +0 -1
- package/dist/PdfAnnotationCanvas.client-JCJMXWWJ.css.map +0 -1
- package/dist/ar-YTUUGK2O.js.map +0 -1
- package/dist/bn-ASGNGPMD.js.map +0 -1
- package/dist/chunk-BTUG3VJJ.js.map +0 -1
- package/dist/chunk-L3WKRWP3.js.map +0 -1
- package/dist/chunk-O23TLLXW.js.map +0 -1
- package/dist/chunk-OBQOZCJE.js.map +0 -1
- package/dist/chunk-QT7LYM72.js.map +0 -1
- package/dist/chunk-VXASXU4K.js.map +0 -1
- package/dist/chunk-XUDKYAVC.js +0 -21
- package/dist/chunk-XUDKYAVC.js.map +0 -1
- package/dist/cs-WMYVAOZY.js.map +0 -1
- package/dist/da-FSHHN4D6.js.map +0 -1
- package/dist/de-FJFO3TPT.js.map +0 -1
- package/dist/el-XOH4QIAG.js.map +0 -1
- package/dist/en-3FCBW3YD.js.map +0 -1
- package/dist/es-HDX4QI7E.js.map +0 -1
- package/dist/fa-DJ6WTFTJ.js.map +0 -1
- package/dist/fi-4S7H7W7U.js.map +0 -1
- package/dist/fr-6UJSYHXC.js.map +0 -1
- package/dist/he-ZZJOHJX2.js.map +0 -1
- package/dist/hi-HZJTAW7U.js.map +0 -1
- package/dist/id-E3I6ILEM.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-ULAPAD7U.js.map +0 -1
- package/dist/ja-SAHZQ4CZ.js.map +0 -1
- package/dist/ko-DFU2ADQ4.js.map +0 -1
- package/dist/ms-NWL76XNT.js.map +0 -1
- package/dist/nl-ATCM76WN.js.map +0 -1
- package/dist/no-UIAQ5RJF.js.map +0 -1
- package/dist/pl-NOECQDXD.js.map +0 -1
- package/dist/pt-SVWIIUPC.js.map +0 -1
- package/dist/ro-NR2DRTZG.js.map +0 -1
- package/dist/sv-STVJ37JP.js.map +0 -1
- package/dist/test-utils.css.map +0 -1
- package/dist/test-utils.js.map +0 -1
- package/dist/th-UWQR4K54.js.map +0 -1
- package/dist/tr-WNXZSD27.js.map +0 -1
- package/dist/uk-SJRMB47Y.js.map +0 -1
- package/dist/vi-TMRQFES2.js.map +0 -1
- package/dist/zh-QOL3LYEM.js.map +0 -1
- package/src/components/modals/ResourceSearchModal.tsx +0 -224
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +0 -159
- package/src/integrations/tailwind-plugin.cjs +0 -224
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
9
|
-
import
|
|
9
|
+
import { useResourceViewedReport } from '../hooks/useResourceViewedReport';
|
|
10
|
+
import type { components, ResourceDescriptor, ResourceId, EventMap } from '@semiont/core';
|
|
10
11
|
import type { ConnectionState } from '@semiont/core';
|
|
11
12
|
import { annotationId } from '@semiont/core';
|
|
12
|
-
import {
|
|
13
|
+
import type { ComposeParams } from '../../../components/modals/ComposeStep';
|
|
14
|
+
import { getLanguage, getPrimaryRepresentation, getPrimaryMediaType, capabilitiesOf, extensionForMediaType } from '@semiont/core';
|
|
13
15
|
import { ANNOTATORS } from '@semiont/react-ui';
|
|
14
16
|
import { ErrorBoundary } from '@semiont/react-ui';
|
|
15
17
|
import { AnnotationHistory } from '@semiont/react-ui';
|
|
@@ -24,9 +26,11 @@ import type { BrowseMediaRenderers, AnnotateMediaRenderers } from '@semiont/reac
|
|
|
24
26
|
import { useObservable } from '@semiont/react-ui';
|
|
25
27
|
import { useResourceContent } from '../../../hooks/useResourceContent';
|
|
26
28
|
import { useMediaToken } from '../../../hooks/useMediaToken';
|
|
29
|
+
import { useCollaborators } from '../../../hooks/useCollaborators';
|
|
27
30
|
import { mediaUrl } from '../../../lib/media-url';
|
|
28
31
|
import { useToast } from '../../../components/Toast';
|
|
29
32
|
import { useOutcomeToasts } from '../../../hooks/useOutcomeToasts';
|
|
33
|
+
import { useGenerationArrival } from '../../../hooks/useGenerationArrival';
|
|
30
34
|
import { useTheme } from '../../../contexts/ThemeContext';
|
|
31
35
|
import { useLineNumbers } from '../../../contexts/LineNumbersContext';
|
|
32
36
|
import { useHoverDelay } from '../../../hooks/useHoverDelay';
|
|
@@ -42,7 +46,6 @@ import { useShellStateUnit } from '../../../hooks/useShellStateUnit';
|
|
|
42
46
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
43
47
|
import { ReferenceWizardModal } from '../../../components/modals/ReferenceWizardModal';
|
|
44
48
|
import { ResourceGenerateModal } from '../../../components/modals/ResourceGenerateModal';
|
|
45
|
-
import { AssistProgress } from '../../../components/AssistProgress';
|
|
46
49
|
import type { GenerationConfig } from '../../../components/modals/ConfigureGenerationStep';
|
|
47
50
|
|
|
48
51
|
type SemiontResource = ResourceDescriptor;
|
|
@@ -117,7 +120,8 @@ export interface ResourceViewerPageProps {
|
|
|
117
120
|
* @subscribes yield:clone - Clone the current resource
|
|
118
121
|
* @subscribes beckon:sparkle - Trigger sparkle animation
|
|
119
122
|
* @subscribes mark:added - Annotation was created (sparkle)
|
|
120
|
-
* @subscribes
|
|
123
|
+
* @subscribes mark:body-updated - Reference resolved via a linking-add operation (sparkle, both loci)
|
|
124
|
+
* @subscribes browse:resource-open - Open a resource in the viewer (local links + the launcher's tour verbs)
|
|
121
125
|
* @subscribes browse:entity-type-clicked - Navigate filtered by entity type
|
|
122
126
|
*
|
|
123
127
|
* Outcome-notification channels (mark:create-error, mark:delete-error,
|
|
@@ -140,13 +144,24 @@ export function ResourceViewerPage({
|
|
|
140
144
|
// Translations
|
|
141
145
|
const tw = useTranslations('ReferenceWizard');
|
|
142
146
|
const tg = useTranslations('ResourceGenerate');
|
|
143
|
-
const ta = useTranslations('AssistProgress');
|
|
144
147
|
|
|
145
148
|
const browser = useSemiont();
|
|
146
149
|
const session = useObservable(browser.activeSession$);
|
|
147
150
|
const semiont = session?.client;
|
|
148
151
|
const navigateExternal = useObservableExternalNavigation();
|
|
149
152
|
|
|
153
|
+
// The KB's collaborator roster, for the Collaboration panel's software-agent
|
|
154
|
+
// rows. Subscribed unconditionally at the top level (hooks ordering) even
|
|
155
|
+
// though only one panel reads it; the underlying cache is a KB-wide
|
|
156
|
+
// singleton, so this costs one shared subscription, not one per panel open.
|
|
157
|
+
const { collaborators } = useCollaborators(semiont ?? null);
|
|
158
|
+
|
|
159
|
+
// At most one entry serves `generation` — the KB's workers.* config yields one
|
|
160
|
+
// inference config per job type — so `find` is exact, not a first-wins guess.
|
|
161
|
+
const generationAgent = collaborators.find((entry) =>
|
|
162
|
+
entry.servesJobTypes?.includes('generation'),
|
|
163
|
+
);
|
|
164
|
+
|
|
150
165
|
// ResourceViewer is bring-your-own-session: feed it the active session plus
|
|
151
166
|
// host-owned navigation (reference follow) and panel control (app-scoped bus).
|
|
152
167
|
const handleViewerOpenResource = useCallback((id: string) => {
|
|
@@ -162,7 +177,7 @@ export function ResourceViewerPage({
|
|
|
162
177
|
const { theme } = useTheme();
|
|
163
178
|
const { showLineNumbers } = useLineNumbers();
|
|
164
179
|
const { hoverDelayMs } = useHoverDelay();
|
|
165
|
-
const { triggerSparkleAnimation,
|
|
180
|
+
const { triggerSparkleAnimation, clearSparkle, sparkleAnnotationIds } = useResourceAnnotations();
|
|
166
181
|
|
|
167
182
|
// Render mode chooses the content path: 'text' decodes inline; 'image'
|
|
168
183
|
// and 'pdf' go through the media-token (binary) path. 'none'/registry-miss
|
|
@@ -224,9 +239,28 @@ export function ResourceViewerPage({
|
|
|
224
239
|
const panelInitialTab = useObservable(stateUnit?.browse.panelInitialTab$) ?? null;
|
|
225
240
|
const onScrollCompleted = stateUnit?.browse.onScrollCompleted;
|
|
226
241
|
const generationProgress = useObservable(stateUnit?.yield.progress$) ?? null;
|
|
242
|
+
const isGenerating = useObservable(stateUnit?.yield.isGenerating$) ?? false;
|
|
243
|
+
const generationOutcome = useObservable(stateUnit?.yield.outcome$) ?? null;
|
|
244
|
+
|
|
245
|
+
// GENERATION-ARRIVAL P2: a completion witnessed on this page reveals the
|
|
246
|
+
// derivation edge the worker minted — the annotations panel opens on
|
|
247
|
+
// References, scrolls to the provenance reference, and its sparkle is
|
|
248
|
+
// re-armed (the mark:added glow burned its window unseen). Never navigates
|
|
249
|
+
// (A2); a held outcome on remount stays quiet (D6, inside the hook).
|
|
250
|
+
const handleGenerationArrival = useCallback((annId: string) => {
|
|
251
|
+
browser.emit('panel:open', { panel: 'annotations', scrollToAnnotationId: annId, motivation: 'linking' });
|
|
252
|
+
triggerSparkleAnimation(annId);
|
|
253
|
+
}, [browser, triggerSparkleAnimation]);
|
|
254
|
+
useGenerationArrival(generationOutcome, annotations, handleGenerationArrival);
|
|
255
|
+
|
|
227
256
|
const gatherContext = useObservable(stateUnit?.gather.context$) ?? null;
|
|
228
257
|
const gatherLoading = useObservable(stateUnit?.gather.loading$) ?? false;
|
|
229
258
|
const gatherError = useObservable(stateUnit?.gather.error$) ?? null;
|
|
259
|
+
// Resource-gather slots (FLOW-LIFECYCLE-CONVERGENCE D2a: separate from the
|
|
260
|
+
// annotation slots above — the two gathers can be live at once).
|
|
261
|
+
const resourceGatherContext = useObservable(stateUnit?.gather.resourceContext$) ?? null;
|
|
262
|
+
const resourceGatherLoading = useObservable(stateUnit?.gather.resourceLoading$) ?? false;
|
|
263
|
+
const resourceGatherError = useObservable(stateUnit?.gather.resourceError$) ?? null;
|
|
230
264
|
const wizardState = useObservable(stateUnit?.wizard$);
|
|
231
265
|
const wizardOpen = wizardState?.open ?? false;
|
|
232
266
|
const wizardAnnotationId = wizardState?.annotationId ?? null;
|
|
@@ -240,8 +274,8 @@ export function ResourceViewerPage({
|
|
|
240
274
|
}, [stateUnit]);
|
|
241
275
|
|
|
242
276
|
const handleWizardGenerateSubmit = useCallback((referenceId: string, config: GenerationConfig) => {
|
|
243
|
-
|
|
244
|
-
stateUnit?.yield.generate(
|
|
277
|
+
clearSparkle(annotationId(referenceId));
|
|
278
|
+
stateUnit?.yield.generate(config.context, {
|
|
245
279
|
title: config.title,
|
|
246
280
|
storageUri: config.storagePath,
|
|
247
281
|
prompt: config.prompt,
|
|
@@ -252,16 +286,15 @@ export function ResourceViewerPage({
|
|
|
252
286
|
sourceLanguage: getLanguage(resource),
|
|
253
287
|
temperature: config.temperature,
|
|
254
288
|
maxTokens: config.maxTokens,
|
|
255
|
-
context: config.context,
|
|
256
289
|
});
|
|
257
|
-
}, [stateUnit,
|
|
290
|
+
}, [stateUnit, clearSparkle, resource]);
|
|
258
291
|
|
|
259
292
|
// Resource-generate flow (GENERATE-FROM-BUTTON): drive the SAME yield progress$
|
|
260
|
-
// the annotation path uses so the full
|
|
261
|
-
//
|
|
262
|
-
//
|
|
293
|
+
// the annotation path uses so the full `AssistProgress` widget shows — NOT a
|
|
294
|
+
// toast. Both paths are one `generate(context, options)` now: the context's
|
|
295
|
+
// focus.kind (resource here, annotation above) decides the shape.
|
|
263
296
|
const handleResourceGenerateSubmit = useCallback((_resourceId: string, config: GenerationConfig) => {
|
|
264
|
-
stateUnit?.yield.
|
|
297
|
+
stateUnit?.yield.generate(config.context, {
|
|
265
298
|
title: config.title,
|
|
266
299
|
storageUri: config.storagePath,
|
|
267
300
|
...(config.prompt ? { prompt: config.prompt } : {}),
|
|
@@ -269,7 +302,6 @@ export function ResourceViewerPage({
|
|
|
269
302
|
sourceLanguage: getLanguage(resource),
|
|
270
303
|
temperature: config.temperature,
|
|
271
304
|
maxTokens: config.maxTokens,
|
|
272
|
-
context: config.context,
|
|
273
305
|
});
|
|
274
306
|
}, [stateUnit, resource]);
|
|
275
307
|
|
|
@@ -287,26 +319,43 @@ export function ResourceViewerPage({
|
|
|
287
319
|
}
|
|
288
320
|
}, [rUri, semiont, showSuccess, showError]);
|
|
289
321
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
322
|
+
// COMPOSE-IN-MODAL P3: create-and-link, in place. The old flow stashed the
|
|
323
|
+
// context in sessionStorage and navigated to the compose page; the modal
|
|
324
|
+
// already holds the context, so the side-channel dies with the mode.
|
|
325
|
+
// Text-only by design — uploads stay on the standalone compose page.
|
|
326
|
+
const handleWizardComposeSubmit = useCallback(async (referenceId: string, params: ComposeParams) => {
|
|
327
|
+
if (!semiont) throw new Error('No active session');
|
|
328
|
+
try {
|
|
329
|
+
const format = 'text/markdown';
|
|
330
|
+
const file = new File(
|
|
331
|
+
[new Blob([params.content], { type: format })],
|
|
332
|
+
params.name + extensionForMediaType(format),
|
|
333
|
+
{ type: format },
|
|
334
|
+
);
|
|
335
|
+
const newResourceId = await new Promise<ResourceId>((resolve, reject) => {
|
|
336
|
+
semiont.yield.resource({
|
|
337
|
+
name: params.name,
|
|
338
|
+
file,
|
|
339
|
+
format,
|
|
340
|
+
entityTypes: params.entityTypes,
|
|
341
|
+
language: params.language,
|
|
342
|
+
storageUri: params.storagePath,
|
|
343
|
+
}).subscribe({
|
|
344
|
+
next: (event) => { if (event.phase === 'finished') resolve(event.resourceId); },
|
|
345
|
+
error: reject,
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
await semiont.bind.body(
|
|
349
|
+
rUri,
|
|
350
|
+
annotationId(referenceId),
|
|
351
|
+
[{ op: 'add', item: { type: 'SpecificResource' as const, source: newResourceId, purpose: 'linking' as const } }],
|
|
352
|
+
);
|
|
353
|
+
showSuccess('Reference successfully linked to the new resource');
|
|
354
|
+
} catch (error) {
|
|
355
|
+
showError(`Failed to compose resource: ${error instanceof Error ? error.message : String(error)}`);
|
|
356
|
+
throw error; // ComposeStep re-enables its footer on rejection
|
|
357
|
+
}
|
|
358
|
+
}, [semiont, rUri, showSuccess, showError]);
|
|
310
359
|
|
|
311
360
|
// Add resource to open tabs when it loads, and record it as this KB's
|
|
312
361
|
// last-viewed for the /know landing route to resume from. Both are per-KB
|
|
@@ -378,7 +427,20 @@ export function ResourceViewerPage({
|
|
|
378
427
|
triggerSparkleAnimation(stored.payload.annotation.id);
|
|
379
428
|
}, [triggerSparkleAnimation]);
|
|
380
429
|
|
|
381
|
-
|
|
430
|
+
// RESOLUTION-SPARKLE D2: a reference resolving — Compose, Search → Link, a
|
|
431
|
+
// Generate job landing, or a remote collaborator — is a body update whose
|
|
432
|
+
// operations add a linking SpecificResource. Exactly that sparkles; an unlink
|
|
433
|
+
// (remove) or an entity-tag body change stays dark.
|
|
434
|
+
const handleAnnotationBodyUpdated = useCallback((stored: EventMap['mark:body-updated']) => {
|
|
435
|
+
const resolves = stored.payload.operations.some(
|
|
436
|
+
(op) => op.op === 'add' && op.item.type === 'SpecificResource' && op.item.purpose === 'linking',
|
|
437
|
+
);
|
|
438
|
+
if (resolves) {
|
|
439
|
+
triggerSparkleAnimation(stored.payload.annotationId);
|
|
440
|
+
}
|
|
441
|
+
}, [triggerSparkleAnimation]);
|
|
442
|
+
|
|
443
|
+
const handleResourceOpen = useCallback(({ resourceId }: { resourceId: string }) => {
|
|
382
444
|
if (routes.resourceDetail) {
|
|
383
445
|
const path = routes.resourceDetail(resourceId);
|
|
384
446
|
browser.emit('nav:push', { path, reason: 'reference-link' });
|
|
@@ -407,7 +469,8 @@ export function ResourceViewerPage({
|
|
|
407
469
|
'yield:clone': handleResourceClone,
|
|
408
470
|
'beckon:sparkle': handleAnnotationSparkle,
|
|
409
471
|
'mark:added': handleAnnotationAdded,
|
|
410
|
-
'
|
|
472
|
+
'mark:body-updated': handleAnnotationBodyUpdated,
|
|
473
|
+
'browse:resource-open': handleResourceOpen,
|
|
411
474
|
'browse:entity-type-clicked': handleEntityTypeClicked,
|
|
412
475
|
});
|
|
413
476
|
|
|
@@ -426,6 +489,11 @@ export function ResourceViewerPage({
|
|
|
426
489
|
}
|
|
427
490
|
}, [contentLoading, content, resource.name, announceResourceLoading, announceResourceLoaded]);
|
|
428
491
|
|
|
492
|
+
// Report the arrival on the wire (browse:resource-viewed) — same
|
|
493
|
+
// load-complete condition the announcement uses, so "viewed" means the
|
|
494
|
+
// content is actually on screen (GUIDED-TOUR P5, D6).
|
|
495
|
+
useResourceViewedReport(rUri, !contentLoading && !!content);
|
|
496
|
+
|
|
429
497
|
// Derived state
|
|
430
498
|
const documentEntityTypes = resource.entityTypes || [];
|
|
431
499
|
|
|
@@ -471,24 +539,6 @@ export function ResourceViewerPage({
|
|
|
471
539
|
</h2>
|
|
472
540
|
</div>
|
|
473
541
|
</div>
|
|
474
|
-
{/* Resource-generation progress (GENERATE-FROM-BUTTON P7) — no annotationId ⇒ a resource-gen job */}
|
|
475
|
-
{generationProgress && !generationProgress.annotationId && (
|
|
476
|
-
<AssistProgress
|
|
477
|
-
progress={generationProgress}
|
|
478
|
-
dataType="generation"
|
|
479
|
-
onCancel={() => session?.client.job.cancelRequest('generation')}
|
|
480
|
-
translations={{
|
|
481
|
-
title: tg('progressTitle'),
|
|
482
|
-
cancel: tg('progressCancel'),
|
|
483
|
-
inProgress: tg('progressInProgress'),
|
|
484
|
-
complete: tg('progressComplete'),
|
|
485
|
-
failed: tg('progressFailed'),
|
|
486
|
-
paramsTitle: ta('paramsTitle'),
|
|
487
|
-
processing: (label) => ta('processing', { label }),
|
|
488
|
-
close: ta('close'),
|
|
489
|
-
}}
|
|
490
|
-
/>
|
|
491
|
-
)}
|
|
492
542
|
{/* Scrollable body wrapper - contains document content, header is sibling above */}
|
|
493
543
|
<div className="semiont-document-viewer__scrollable-body" lang={getLanguage(resource) || undefined}>
|
|
494
544
|
<ErrorBoundary
|
|
@@ -528,7 +578,7 @@ export function ResourceViewerPage({
|
|
|
528
578
|
onSelectionMotivationChange={toolbarPrefs.setSelectionMotivation}
|
|
529
579
|
shape={toolbarPrefs.shape}
|
|
530
580
|
onShapeChange={toolbarPrefs.setShape}
|
|
531
|
-
|
|
581
|
+
sparkleAnnotationIds={sparkleAnnotationIds}
|
|
532
582
|
generatingReferenceId={generationProgress?.annotationId ?? null}
|
|
533
583
|
showLineNumbers={showLineNumbers}
|
|
534
584
|
hoverDelayMs={hoverDelayMs}
|
|
@@ -578,6 +628,7 @@ export function ResourceViewerPage({
|
|
|
578
628
|
onRetryAnnotations={stateUnit?.annotations.retry}
|
|
579
629
|
entityTypesError={entityTypesError}
|
|
580
630
|
generatingReferenceId={generationProgress?.annotationId ?? null}
|
|
631
|
+
sparkleAnnotationIds={sparkleAnnotationIds}
|
|
581
632
|
referencedBy={referencedBy}
|
|
582
633
|
referencedByLoading={referencedByLoading}
|
|
583
634
|
referencedByError={referencedByError}
|
|
@@ -588,7 +639,7 @@ export function ResourceViewerPage({
|
|
|
588
639
|
scrollToAnnotationId={scrollToAnnotationId}
|
|
589
640
|
hoveredAnnotationId={hoveredAnnotationId}
|
|
590
641
|
onScrollCompleted={onScrollCompleted}
|
|
591
|
-
initialTab={panelInitialTab?.tab
|
|
642
|
+
initialTab={panelInitialTab?.tab}
|
|
592
643
|
initialTabGeneration={panelInitialTab?.generation}
|
|
593
644
|
Link={Link}
|
|
594
645
|
routes={routes}
|
|
@@ -628,6 +679,15 @@ export function ResourceViewerPage({
|
|
|
628
679
|
wasDerivedFrom={resource.wasDerivedFrom}
|
|
629
680
|
generator={resource.generator as components['schemas']['Agent'] | components['schemas']['Agent'][] | undefined}
|
|
630
681
|
onGenerate={() => setGenerateOpen(true)}
|
|
682
|
+
// The panel is generation's progress surface (GENERATE-FROM-
|
|
683
|
+
// RESOURCE D7); no annotationId ⇒ a resource-gen job — the
|
|
684
|
+
// annotation path's frame renders in the reference wizard.
|
|
685
|
+
isGenerating={isGenerating}
|
|
686
|
+
generationProgress={
|
|
687
|
+
generationProgress && !generationProgress.annotationId ? generationProgress : null
|
|
688
|
+
}
|
|
689
|
+
generationOutcome={generationOutcome}
|
|
690
|
+
onDismissProgress={() => stateUnit?.yield.dismissProgress()}
|
|
631
691
|
/>
|
|
632
692
|
)}
|
|
633
693
|
|
|
@@ -637,6 +697,7 @@ export function ResourceViewerPage({
|
|
|
637
697
|
state={streamStatus}
|
|
638
698
|
eventCount={0}
|
|
639
699
|
knowledgeBaseName={knowledgeBaseName}
|
|
700
|
+
collaborators={collaborators}
|
|
640
701
|
/>
|
|
641
702
|
)}
|
|
642
703
|
|
|
@@ -657,32 +718,40 @@ export function ResourceViewerPage({
|
|
|
657
718
|
|
|
658
719
|
{/* Reference Resolution Wizard */}
|
|
659
720
|
<ReferenceWizardModal
|
|
721
|
+
{...(generationAgent ? { generationAgent } : {})}
|
|
660
722
|
isOpen={wizardOpen}
|
|
661
723
|
onClose={handleWizardClose}
|
|
662
724
|
annotationId={wizardAnnotationId}
|
|
663
725
|
resourceId={wizardResourceId}
|
|
664
726
|
defaultTitle={wizardDefaultTitle}
|
|
665
727
|
entityTypes={wizardEntityTypes}
|
|
728
|
+
resourceName={resource.name}
|
|
666
729
|
locale={locale}
|
|
667
730
|
context={gatherContext}
|
|
668
731
|
contextLoading={gatherLoading}
|
|
669
732
|
contextError={gatherError}
|
|
670
733
|
onGenerateSubmit={handleWizardGenerateSubmit}
|
|
671
734
|
onLinkResource={handleWizardLinkResource}
|
|
672
|
-
|
|
735
|
+
onComposeSubmit={handleWizardComposeSubmit}
|
|
736
|
+
entityTypeOptions={allEntityTypes}
|
|
737
|
+
hoverDelayMs={hoverDelayMs}
|
|
673
738
|
translations={{
|
|
674
|
-
|
|
675
|
-
configureGenerationTitle: tw('configureGenerationTitle'),
|
|
676
|
-
configureSearchTitle: tw('configureSearchTitle'),
|
|
677
|
-
searchResultsTitle: tw('searchResultsTitle'),
|
|
739
|
+
resolveTitle: tw('resolveTitle'),
|
|
678
740
|
sourceContextLabel: tw('sourceContextLabel'),
|
|
679
741
|
connectionsLabel: tw('connectionsLabel'),
|
|
680
742
|
citedByLabel: tw('citedByLabel'),
|
|
681
743
|
userHintLabel: tw('userHintLabel'),
|
|
744
|
+
userHintEffect: tw('userHintEffect'),
|
|
682
745
|
userHintPlaceholder: tw('userHintPlaceholder'),
|
|
746
|
+
graphPaneTitle: tw('graphPaneTitle'),
|
|
747
|
+
graphEmpty: tw('graphEmpty'),
|
|
748
|
+
resourceLinkLabel: tw('resourceLinkLabel'),
|
|
749
|
+
corpusPaneTitle: tw('corpusPaneTitle'),
|
|
750
|
+
corpusEmpty: tw('corpusEmpty'),
|
|
751
|
+
excludedReceipt: tw('excludedReceipt'),
|
|
752
|
+
machineRead: tw('machineRead'),
|
|
683
753
|
loadingContext: tw('loadingContext'),
|
|
684
754
|
failedContext: tw('failedContext'),
|
|
685
|
-
cancel: tw('cancel'),
|
|
686
755
|
search: tw('search'),
|
|
687
756
|
searching: tw('searching'),
|
|
688
757
|
generate: tw('generate'),
|
|
@@ -694,6 +763,7 @@ export function ResourceViewerPage({
|
|
|
694
763
|
noResults: tw('noResults'),
|
|
695
764
|
resourceTitle: tw('resourceTitle'),
|
|
696
765
|
resourceTitlePlaceholder: tw('resourceTitlePlaceholder'),
|
|
766
|
+
saveLocation: tw('saveLocation'),
|
|
697
767
|
additionalInstructions: tw('additionalInstructions'),
|
|
698
768
|
additionalInstructionsPlaceholder: tw('additionalInstructionsPlaceholder'),
|
|
699
769
|
language: tw('language'),
|
|
@@ -703,42 +773,63 @@ export function ResourceViewerPage({
|
|
|
703
773
|
creativityCreative: tw('creativityCreative'),
|
|
704
774
|
maxLength: tw('maxLength'),
|
|
705
775
|
maxLengthHelp: tw('maxLengthHelp'),
|
|
776
|
+
maxLengthCeiling: tw('maxLengthCeiling'),
|
|
706
777
|
maxResults: tw('maxResults'),
|
|
707
778
|
semanticScoring: tw('semanticScoring'),
|
|
708
779
|
semanticScoringHelp: tw('semanticScoringHelp'),
|
|
780
|
+
searchFailed: tw('searchFailed'),
|
|
781
|
+
entityTypes: tw('entityTypes'),
|
|
782
|
+
contentLabel: tw('contentLabel'),
|
|
783
|
+
createAndLink: tw('createAndLink'),
|
|
784
|
+
creatingAndLinking: tw('creatingAndLinking'),
|
|
785
|
+
discardDraftPrompt: tw('discardDraftPrompt'),
|
|
786
|
+
discardDraft: tw('discardDraft'),
|
|
787
|
+
keepEditing: tw('keepEditing'),
|
|
709
788
|
}}
|
|
710
789
|
/>
|
|
711
790
|
|
|
712
791
|
{/* Resource-generate flow (GENERATE-FROM-BUTTON) */}
|
|
713
792
|
<ResourceGenerateModal
|
|
793
|
+
{...(generationAgent ? { generationAgent } : {})}
|
|
714
794
|
isOpen={generateOpen}
|
|
715
795
|
onClose={() => setGenerateOpen(false)}
|
|
716
796
|
resourceId={rUri}
|
|
717
|
-
|
|
797
|
+
// Seed the proposed title from the source resource's name (GFR D4/A4);
|
|
798
|
+
// the field stays editable and required.
|
|
799
|
+
defaultTitle={resource.name}
|
|
718
800
|
locale={locale}
|
|
719
801
|
entityTypeOptions={allEntityTypes}
|
|
720
802
|
onGenerateSubmit={handleResourceGenerateSubmit}
|
|
803
|
+
gatherContext={resourceGatherContext}
|
|
804
|
+
gatherLoading={resourceGatherLoading}
|
|
805
|
+
gatherError={resourceGatherError}
|
|
806
|
+
onGather={(options) => stateUnit?.gather.gatherResource(rUri, options)}
|
|
721
807
|
translations={{
|
|
722
|
-
|
|
723
|
-
reviewTitle: tg('reviewTitle'),
|
|
724
|
-
configureTitle: tg('configureTitle'),
|
|
725
|
-
next: tg('next'),
|
|
726
|
-
back: tg('back'),
|
|
727
|
-
cancel: tg('cancel'),
|
|
808
|
+
title: tg('title'),
|
|
728
809
|
gatherIntro: tg('gatherIntro'),
|
|
729
810
|
includeContent: tg('includeContent'),
|
|
730
811
|
includeSummary: tg('includeSummary'),
|
|
731
812
|
gatherDepth: tg('gatherDepth'),
|
|
732
813
|
gatherMaxResources: tg('gatherMaxResources'),
|
|
733
814
|
gatherButton: tg('gatherButton'),
|
|
734
|
-
|
|
815
|
+
editGather: tg('editGather'),
|
|
816
|
+
recallLabel: tg('recallLabel'),
|
|
735
817
|
loadingContext: tg('loadingContext'),
|
|
736
818
|
failedContext: tg('failedContext'),
|
|
737
819
|
sourceContextLabel: tg('sourceContextLabel'),
|
|
738
820
|
connectionsLabel: tg('connectionsLabel'),
|
|
739
821
|
citedByLabel: tg('citedByLabel'),
|
|
822
|
+
graphPaneTitle: tg('graphPaneTitle'),
|
|
823
|
+
graphEmpty: tg('graphEmpty'),
|
|
824
|
+
resourceLinkLabel: tg('resourceLinkLabel'),
|
|
825
|
+
corpusPaneTitle: tg('corpusPaneTitle'),
|
|
826
|
+
corpusEmpty: tg('corpusEmpty'),
|
|
827
|
+
excludedReceipt: tg('excludedReceipt'),
|
|
828
|
+
machineRead: tg('machineRead'),
|
|
829
|
+
score: tg('score'),
|
|
740
830
|
resourceTitle: tg('resourceTitle'),
|
|
741
831
|
resourceTitlePlaceholder: tg('resourceTitlePlaceholder'),
|
|
832
|
+
saveLocation: tg('saveLocation'),
|
|
742
833
|
additionalInstructions: tg('additionalInstructions'),
|
|
743
834
|
additionalInstructionsPlaceholder: tg('additionalInstructionsPlaceholder'),
|
|
744
835
|
language: tg('language'),
|
|
@@ -748,7 +839,11 @@ export function ResourceViewerPage({
|
|
|
748
839
|
creativityCreative: tg('creativityCreative'),
|
|
749
840
|
maxLength: tg('maxLength'),
|
|
750
841
|
maxLengthHelp: tg('maxLengthHelp'),
|
|
842
|
+
maxLengthCeiling: tg('maxLengthCeiling'),
|
|
751
843
|
generate: tg('generate'),
|
|
844
|
+
discardDraftPrompt: tg('discardDraftPrompt'),
|
|
845
|
+
discardDraft: tg('discardDraft'),
|
|
846
|
+
keepEditing: tg('keepEditing'),
|
|
752
847
|
}}
|
|
753
848
|
/>
|
|
754
849
|
</div>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The viewer's arrival report — GUIDED-TOUR P5 (D6).
|
|
3
|
+
*
|
|
4
|
+
* `browse:resource-viewed` is a REPORT, deliberately distinct from the
|
|
5
|
+
* imperative `browse:resource-open`: it fires when a resource has actually
|
|
6
|
+
* loaded on screen, however the user got there (followed cue, in-app link,
|
|
7
|
+
* back button, typed URL). The contract this pins: once per arrival — never
|
|
8
|
+
* on unrelated re-renders, never before the content is up, and again when
|
|
9
|
+
* the viewer moves to a different resource.
|
|
10
|
+
*/
|
|
11
|
+
import { renderHook } from '@testing-library/react';
|
|
12
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
13
|
+
import type { ReactNode } from 'react';
|
|
14
|
+
import { useResourceViewedReport } from '../useResourceViewedReport';
|
|
15
|
+
import { createTestSemiontWrapper } from '../../../../test-utils';
|
|
16
|
+
import { resourceId } from '@semiont/core';
|
|
17
|
+
|
|
18
|
+
function makeWrapper() {
|
|
19
|
+
const { SemiontWrapper, client } = createTestSemiontWrapper();
|
|
20
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
21
|
+
<SemiontWrapper>{children}</SemiontWrapper>
|
|
22
|
+
);
|
|
23
|
+
const viewed = vi.spyOn(client.browse, 'resourceViewed').mockImplementation(() => {});
|
|
24
|
+
return { wrapper, viewed };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const RID_A = resourceId('resaaa11223344556677889900aabbcc');
|
|
28
|
+
const RID_B = resourceId('resbbb11223344556677889900aabbcc');
|
|
29
|
+
|
|
30
|
+
describe('useResourceViewedReport', () => {
|
|
31
|
+
it('reports once when the resource finishes loading, not before and not again on re-render', () => {
|
|
32
|
+
const { wrapper, viewed } = makeWrapper();
|
|
33
|
+
|
|
34
|
+
const { rerender } = renderHook(
|
|
35
|
+
({ rid, loaded }: { rid: ReturnType<typeof resourceId>; loaded: boolean }) => useResourceViewedReport(rid, loaded),
|
|
36
|
+
{ wrapper, initialProps: { rid: RID_A, loaded: false } },
|
|
37
|
+
);
|
|
38
|
+
expect(viewed).not.toHaveBeenCalled();
|
|
39
|
+
|
|
40
|
+
rerender({ rid: RID_A, loaded: true });
|
|
41
|
+
expect(viewed).toHaveBeenCalledTimes(1);
|
|
42
|
+
|
|
43
|
+
rerender({ rid: RID_A, loaded: true });
|
|
44
|
+
rerender({ rid: RID_A, loaded: true });
|
|
45
|
+
expect(viewed).toHaveBeenCalledTimes(1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('reports again when the viewer arrives at a different resource', () => {
|
|
49
|
+
const { wrapper, viewed } = makeWrapper();
|
|
50
|
+
|
|
51
|
+
const { rerender } = renderHook(
|
|
52
|
+
({ rid, loaded }: { rid: ReturnType<typeof resourceId>; loaded: boolean }) => useResourceViewedReport(rid, loaded),
|
|
53
|
+
{ wrapper, initialProps: { rid: RID_A, loaded: true } },
|
|
54
|
+
);
|
|
55
|
+
expect(viewed).toHaveBeenCalledTimes(1);
|
|
56
|
+
|
|
57
|
+
rerender({ rid: RID_B, loaded: false });
|
|
58
|
+
expect(viewed).toHaveBeenCalledTimes(1);
|
|
59
|
+
|
|
60
|
+
rerender({ rid: RID_B, loaded: true });
|
|
61
|
+
expect(viewed).toHaveBeenCalledTimes(2);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import type { ResourceId } from '@semiont/core';
|
|
3
|
+
import { useSemiont } from '../../../session/SemiontProvider';
|
|
4
|
+
import { useObservable } from '../../../hooks/useObservable';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Report a resource arrival on `browse:resource-viewed` — GUIDED-TOUR P5 (D6).
|
|
8
|
+
*
|
|
9
|
+
* Fires exactly once per arrival, when the resource has finished loading —
|
|
10
|
+
* however the user got here: a followed cue, an in-app link, the back button,
|
|
11
|
+
* a typed URL. That "any means" property is why this lives at the viewer's
|
|
12
|
+
* load-complete transition rather than on any navigation intent path. A
|
|
13
|
+
* re-render never re-reports; arriving at a different resource does.
|
|
14
|
+
*
|
|
15
|
+
* This is the REPORT half of the tour protocol. The imperative half is
|
|
16
|
+
* `browse:resource-open`; they must never share a channel (D6 — the driver
|
|
17
|
+
* would hear its own commands, and one viewer's click would steer another's
|
|
18
|
+
* page).
|
|
19
|
+
*/
|
|
20
|
+
export function useResourceViewedReport(rid: ResourceId, loaded: boolean): void {
|
|
21
|
+
const semiont = useSemiont();
|
|
22
|
+
const session = useObservable(semiont.activeSession$);
|
|
23
|
+
// Brand once at the boundary (BRAND-UPSTREAM): the caller already holds a
|
|
24
|
+
// ResourceId; this hook never re-brands.
|
|
25
|
+
const reported = useRef<ResourceId | null>(null);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!loaded || !session) return;
|
|
29
|
+
if (reported.current === rid) return;
|
|
30
|
+
reported.current = rid;
|
|
31
|
+
session.client.browse.resourceViewed(rid);
|
|
32
|
+
}, [rid, loaded, session]);
|
|
33
|
+
}
|
package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts
CHANGED
|
@@ -56,7 +56,7 @@ function clientWithNamespaces(overrides: {
|
|
|
56
56
|
},
|
|
57
57
|
gather: { annotation: vi.fn(() => new Observable(() => {})) },
|
|
58
58
|
match: { search: vi.fn(() => new Observable(() => {})) },
|
|
59
|
-
yield: {
|
|
59
|
+
yield: { fromContext: vi.fn(() => new Observable(() => {})) },
|
|
60
60
|
bind: { body: vi.fn().mockResolvedValue(undefined) },
|
|
61
61
|
});
|
|
62
62
|
}
|
|
@@ -357,7 +357,7 @@ describe('createResourceViewerPageStateUnit — list failure states', () => {
|
|
|
357
357
|
mark: { annotation: vi.fn(), delete: vi.fn(), assist: vi.fn(() => new Observable(() => {})) },
|
|
358
358
|
gather: { annotation: vi.fn(() => new Observable(() => {})) },
|
|
359
359
|
match: { search: vi.fn(() => new Observable(() => {})) },
|
|
360
|
-
yield: {
|
|
360
|
+
yield: { fromContext: vi.fn(() => new Observable(() => {})) },
|
|
361
361
|
bind: { body: vi.fn().mockResolvedValue(undefined) },
|
|
362
362
|
});
|
|
363
363
|
const unit = createResourceViewerPageStateUnit(tc.session, RID, 'en', mockBrowse());
|
|
@@ -76,7 +76,7 @@ export function createResourceViewerPageStateUnit(
|
|
|
76
76
|
const mark = createMarkStateUnit(client, resourceId);
|
|
77
77
|
const gather = createGatherStateUnit(client, resourceId);
|
|
78
78
|
const matchStateUnit = createMatchStateUnit(client, resourceId);
|
|
79
|
-
const yieldStateUnit = createYieldStateUnit(client,
|
|
79
|
+
const yieldStateUnit = createYieldStateUnit(client, locale);
|
|
80
80
|
|
|
81
81
|
disposer.add(beckon);
|
|
82
82
|
// `browse` (ShellStateUnit) is a *passed-in* dependency — owned by `useShellStateUnit`,
|
|
@@ -487,8 +487,10 @@
|
|
|
487
487
|
color: var(--semiont-color-white);
|
|
488
488
|
}
|
|
489
489
|
|
|
490
|
+
/* Hover lifts. The dark active state is primary-600, so hovering to 700 sank
|
|
491
|
+
it — the opposite of the sibling .semiont-panel-button:hover above. */
|
|
490
492
|
[data-theme="dark"] .semiont-panel-button-active:hover {
|
|
491
|
-
background-color: var(--semiont-color-primary-
|
|
493
|
+
background-color: var(--semiont-color-primary-500);
|
|
492
494
|
}
|
|
493
495
|
|
|
494
496
|
/* ============================================
|