@semiont/react-ui 0.5.10 → 0.5.12
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 +1 -1
- package/dist/{PdfAnnotationCanvas.client-P37SXBWQ.js → PdfAnnotationCanvas.client-75GY2EDR.js} +15 -6
- package/dist/PdfAnnotationCanvas.client-75GY2EDR.js.map +1 -0
- package/dist/{chunk-5LFFAVDI.js → chunk-PFUXQPWJ.js} +96 -96
- package/dist/{chunk-5LFFAVDI.js.map → chunk-PFUXQPWJ.js.map} +1 -1
- package/dist/{chunk-O2MD7TGE.js → chunk-VXASXU4K.js} +10 -2
- package/dist/chunk-VXASXU4K.js.map +1 -0
- package/dist/index.css +150 -112
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +334 -48
- package/dist/index.js +2494 -2201
- package/dist/index.js.map +1 -1
- package/dist/test-utils.d.ts +5 -1
- package/dist/test-utils.js +10 -4
- package/dist/test-utils.js.map +1 -1
- package/package.json +7 -7
- package/src/components/ResourceTagsInline.tsx +94 -11
- package/src/components/StatusDisplay.css +36 -0
- package/src/components/__tests__/ResourceTagsInline.test.tsx +101 -3
- package/src/components/annotation/AnnotateToolbar.tsx +53 -28
- package/src/components/annotation/__tests__/AnnotateToolbar.display.test.tsx +76 -0
- package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +124 -360
- package/src/components/image-annotation/AnnotationOverlay.tsx +6 -6
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +17 -5
- package/src/components/image-annotation/__tests__/AnnotationOverlay.click.test.tsx +137 -0
- package/src/components/image-annotation/__tests__/SvgDrawingCanvas.click.test.tsx +108 -0
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +17 -4
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +143 -7
- package/src/components/resource/AnnotateView.tsx +59 -38
- package/src/components/resource/BrowseView.tsx +172 -138
- package/src/components/resource/ResourceViewer.tsx +123 -130
- package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +55 -0
- package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +49 -0
- package/src/components/resource/__tests__/BrowseView.links.test.tsx +60 -0
- package/src/components/resource/__tests__/BrowseView.overlay-async.test.tsx +113 -0
- package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +161 -0
- package/src/components/resource/__tests__/BrowseView.registry.test.tsx +43 -0
- package/src/components/resource/__tests__/BrowseView.test.tsx +133 -53
- package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +102 -0
- package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +12 -7
- package/src/components/resource/__tests__/ResourceViewer.toolbar-opt-out.test.tsx +135 -0
- package/src/components/resource/__tests__/ResourceViewer.toolbar-prefs.test.tsx +200 -0
- package/src/components/resource/__tests__/browse-renderers.dispatch.test.tsx +113 -0
- package/src/components/resource/__tests__/browse-renderers.test.tsx +34 -0
- package/src/components/resource/__tests__/inline-embedding.test.tsx +81 -0
- package/src/components/resource/__tests__/media-completeness.test.tsx +88 -0
- package/src/components/resource/browse-renderers.tsx +85 -0
- package/src/components/resource/panels/AssessmentEntry.tsx +5 -4
- package/src/components/resource/panels/AssessmentPanel.tsx +12 -5
- package/src/components/resource/panels/AssistSection.tsx +4 -3
- package/src/components/resource/panels/CommentEntry.tsx +5 -4
- package/src/components/resource/panels/CommentsPanel.tsx +12 -5
- package/src/components/resource/panels/HighlightEntry.tsx +5 -4
- package/src/components/resource/panels/HighlightPanel.tsx +12 -5
- package/src/components/resource/panels/ReferenceEntry.tsx +9 -10
- package/src/components/resource/panels/ReferencesPanel.tsx +15 -6
- package/src/components/resource/panels/ResourceInfoPanel.tsx +4 -3
- package/src/components/resource/panels/TagEntry.tsx +5 -3
- package/src/components/resource/panels/TaggingPanel.tsx +11 -4
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +15 -2
- package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +28 -20
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +39 -28
- package/src/components/resource/panels/__tests__/AssistSection.test.tsx +46 -17
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +65 -57
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +54 -24
- package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +27 -17
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +23 -17
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +48 -77
- package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +87 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +15 -9
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +78 -59
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +11 -4
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +33 -19
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +16 -7
- package/src/components/toolbar/Toolbar.css +14 -0
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +7 -17
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +43 -17
- package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +2 -14
- package/src/styles/features/resource-viewer.css +19 -0
- package/dist/PdfAnnotationCanvas.client-P37SXBWQ.js.map +0 -1
- package/dist/chunk-O2MD7TGE.js.map +0 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EMBEDDABLE-VIEWER-COMPLETION Phase 4 — media-completeness sweep.
|
|
3
|
+
*
|
|
4
|
+
* ONE mounted component (`ResourceViewer`, inline, bare session, no providers)
|
|
5
|
+
* renders every standard media type from the DEFAULTS — text, markdown, image,
|
|
6
|
+
* pdf — with the annotation-overlay container present and zero per-type wiring.
|
|
7
|
+
* (Formatted-markdown correctness is pinned by browse-renderers.test; visual
|
|
8
|
+
* overlay alignment is the plan's live smoke-test. This sweep pins the routing
|
|
9
|
+
* parity: every type reaches its renderer from one component.)
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
12
|
+
import { render, screen } from '@testing-library/react';
|
|
13
|
+
import '@testing-library/jest-dom';
|
|
14
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
15
|
+
import type { ResourceDescriptor as SemiontResource, ResourceId } from '@semiont/core';
|
|
16
|
+
import { ResourceViewer } from '../ResourceViewer';
|
|
17
|
+
|
|
18
|
+
vi.mock('react-markdown', () => ({ default: ({ children }: { children: string }) => <div data-testid="md">{children}</div> }));
|
|
19
|
+
vi.mock('remark-gfm', () => ({ default: () => ({}) }));
|
|
20
|
+
// pdfjs can't load in jsdom — the sweep pins ROUTING to the pdf renderer, not pdf.js itself.
|
|
21
|
+
vi.mock('../../pdf-annotation/PdfAnnotationCanvas.client', () => ({
|
|
22
|
+
PdfAnnotationCanvas: ({ pdfUrl }: { pdfUrl: string }) => <div data-testid="pdf-canvas">{pdfUrl}</div>,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const emptyAnnotations = { highlights: [], references: [], assessments: [], comments: [], tags: [] };
|
|
26
|
+
|
|
27
|
+
function fakeSession(): SemiontSession {
|
|
28
|
+
return {
|
|
29
|
+
client: { browse: { click: vi.fn() }, beckon: { hover: vi.fn() } },
|
|
30
|
+
subscribe: () => () => {},
|
|
31
|
+
} as unknown as SemiontSession;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeResource(mediaType: string, content: string): SemiontResource & { content: string } {
|
|
35
|
+
return {
|
|
36
|
+
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
37
|
+
'@id': 'res-1' as ResourceId,
|
|
38
|
+
name: 'Doc',
|
|
39
|
+
created: '2024-01-01T00:00:00Z',
|
|
40
|
+
entityTypes: [],
|
|
41
|
+
archived: false,
|
|
42
|
+
representations: [{ mediaType, byteSize: 10 }],
|
|
43
|
+
content,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mount(mediaType: string, content: string) {
|
|
48
|
+
return render(
|
|
49
|
+
<ResourceViewer
|
|
50
|
+
resource={makeResource(mediaType, content)}
|
|
51
|
+
annotations={emptyAnnotations}
|
|
52
|
+
session={fakeSession()}
|
|
53
|
+
inline
|
|
54
|
+
/>,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('media-completeness sweep (Phase 4) — one component, all types, inline, defaults only', () => {
|
|
59
|
+
it.each([
|
|
60
|
+
['text/plain', 'plain body'],
|
|
61
|
+
['text/markdown', '# md body'],
|
|
62
|
+
])('%s renders via the text default with the overlay container', (mt, content) => {
|
|
63
|
+
const { container } = mount(mt, content);
|
|
64
|
+
expect(container.querySelector('.semiont-browse-view[data-mime-type="text"]')).toBeInTheDocument();
|
|
65
|
+
expect(screen.getByTestId('md')).toHaveTextContent(content.replace('# ', '# ').trim());
|
|
66
|
+
expect(container.querySelector('.semiont-browse-view__content')).toBeInTheDocument(); // overlay anchor
|
|
67
|
+
expect(container.querySelector('.semiont-browse-view')).toHaveClass('semiont-browse-view--inline');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('image/png renders via the image default', () => {
|
|
71
|
+
const { container } = mount('image/png', 'https://media.test/img.png?token=t');
|
|
72
|
+
expect(container.querySelector('.semiont-browse-view[data-mime-type="image"]')).toBeInTheDocument();
|
|
73
|
+
expect(container.querySelector('img')).toBeInTheDocument();
|
|
74
|
+
expect(container.querySelector('.semiont-browse-view__content')).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('application/pdf routes to the pdf default', async () => {
|
|
78
|
+
const { container } = mount('application/pdf', 'https://media.test/doc.pdf?token=t');
|
|
79
|
+
expect(container.querySelector('.semiont-browse-view[data-mime-type="pdf"]')).toBeInTheDocument();
|
|
80
|
+
expect(await screen.findByTestId('pdf-canvas')).toHaveTextContent('https://media.test/doc.pdf?token=t');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('an unknown type degrades to metadata + download (no crash, no per-type wiring)', () => {
|
|
84
|
+
const { container } = mount('application/octet-stream', '');
|
|
85
|
+
expect(container.querySelector('[data-mime-type="unsupported"]')).toBeInTheDocument();
|
|
86
|
+
expect(screen.getByText(/Preview not available/)).toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { lazy, Suspense, memo, type ComponentType } from 'react';
|
|
4
|
+
import ReactMarkdown from 'react-markdown';
|
|
5
|
+
import remarkGfm from 'remark-gfm';
|
|
6
|
+
import type { Annotation } from '@semiont/core';
|
|
7
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
8
|
+
import { SvgDrawingCanvas } from '../image-annotation/SvgDrawingCanvas';
|
|
9
|
+
|
|
10
|
+
// Lazy-load the PDF component to avoid SSR issues with browser PDF.js loading.
|
|
11
|
+
const PdfAnnotationCanvas = lazy(() => import('../pdf-annotation/PdfAnnotationCanvas.client').then(mod => ({ default: mod.PdfAnnotationCanvas })));
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Common props for a read-only ("browse") media renderer. `content` is decoded
|
|
15
|
+
* text for the text renderer, or a media URL for the image / PDF renderers.
|
|
16
|
+
* A consumer can implement this interface to swap in a custom renderer.
|
|
17
|
+
*/
|
|
18
|
+
export interface MediaRendererProps {
|
|
19
|
+
content: string;
|
|
20
|
+
mimeType: string;
|
|
21
|
+
resourceUri: string;
|
|
22
|
+
annotations: Annotation[];
|
|
23
|
+
/**
|
|
24
|
+
* Session for interaction routing inside annotation-bearing renderers — the
|
|
25
|
+
* canvases emit clicks/hover via `session.client.browse/beckon`. Absent/null
|
|
26
|
+
* → the annotations still PAINT, but are inert (paint-only). Threaded by
|
|
27
|
+
* BrowseView's dispatch; custom renderers may ignore it.
|
|
28
|
+
*/
|
|
29
|
+
session?: SemiontSession | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Read-only media dispatch, keyed by the registry render mode. */
|
|
33
|
+
export type BrowseMediaRenderers = Partial<Record<string, ComponentType<MediaRendererProps>>>;
|
|
34
|
+
|
|
35
|
+
const MemoizedMarkdown = memo(function MemoizedMarkdown({ content }: { content: string }) {
|
|
36
|
+
// No annotation plugins: annotations are applied as a DOM overlay after paint.
|
|
37
|
+
return <ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export function TextBrowseRenderer({ content }: MediaRendererProps) {
|
|
41
|
+
return <MemoizedMarkdown content={content} />;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ImageBrowseRenderer({ content, resourceUri, annotations, session }: MediaRendererProps) {
|
|
45
|
+
// The annotate-mode canvas, read-only (drawingMode=null): paints SvgSelector
|
|
46
|
+
// shapes over the image and routes click/hover via the session — the same
|
|
47
|
+
// shapes browse mode silently dropped when this was a bare ImageViewer
|
|
48
|
+
// (bugs/image-browse-renderer-drops-annotations.md).
|
|
49
|
+
return (
|
|
50
|
+
<SvgDrawingCanvas
|
|
51
|
+
imageUrl={content}
|
|
52
|
+
resourceUri={resourceUri}
|
|
53
|
+
existingAnnotations={annotations}
|
|
54
|
+
drawingMode={null}
|
|
55
|
+
selectedMotivation={null}
|
|
56
|
+
session={session}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function PdfBrowseRenderer({ content, resourceUri, annotations, session }: MediaRendererProps) {
|
|
62
|
+
return (
|
|
63
|
+
<Suspense fallback={<div className="semiont-browse-view__loading">Loading PDF viewer...</div>}>
|
|
64
|
+
<PdfAnnotationCanvas
|
|
65
|
+
pdfUrl={content}
|
|
66
|
+
resourceUri={resourceUri}
|
|
67
|
+
existingAnnotations={annotations}
|
|
68
|
+
drawingMode={null}
|
|
69
|
+
selectedMotivation={null}
|
|
70
|
+
session={session}
|
|
71
|
+
/>
|
|
72
|
+
</Suspense>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Default read-only media renderers. `BrowseView` merges a caller's `renderers`
|
|
78
|
+
* override on top of these, so a host can swap one renderer (e.g. its own PDF
|
|
79
|
+
* viewer) or add a mode without forking the view.
|
|
80
|
+
*/
|
|
81
|
+
export const defaultBrowseRenderers: Record<'text' | 'image' | 'pdf', ComponentType<MediaRendererProps>> = {
|
|
82
|
+
text: TextBrowseRenderer,
|
|
83
|
+
image: ImageBrowseRenderer,
|
|
84
|
+
pdf: PdfBrowseRenderer,
|
|
85
|
+
};
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
import type { Ref } from 'react';
|
|
4
4
|
import type { Annotation } from '@semiont/core';
|
|
5
5
|
import { getAnnotationExactText } from '@semiont/core';
|
|
6
|
-
import {
|
|
7
|
-
import { useObservable } from '../../../hooks/useObservable';
|
|
6
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
8
7
|
import { useHoverEmitter } from '../../../hooks/useHoverEmitter';
|
|
9
8
|
import { renderAgentLabel } from './agent-label';
|
|
10
9
|
|
|
@@ -17,6 +16,8 @@ interface TextualBody {
|
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
interface AssessmentEntryProps {
|
|
19
|
+
/** Session for interaction routing (browse.click etc.); the panel threads it. */
|
|
20
|
+
session: SemiontSession | null;
|
|
20
21
|
assessment: Annotation;
|
|
21
22
|
isFocused: boolean;
|
|
22
23
|
isHovered?: boolean;
|
|
@@ -69,13 +70,13 @@ function getAssessmentText(annotation: Annotation): string | null {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
export function AssessmentEntry({
|
|
73
|
+
session,
|
|
72
74
|
assessment,
|
|
73
75
|
isFocused,
|
|
74
76
|
isHovered = false,
|
|
75
77
|
ref,
|
|
76
78
|
}: AssessmentEntryProps) {
|
|
77
|
-
const
|
|
78
|
-
const hoverProps = useHoverEmitter(assessment.id);
|
|
79
|
+
const hoverProps = useHoverEmitter(session, assessment.id);
|
|
79
80
|
|
|
80
81
|
const selectedText = getAnnotationExactText(assessment);
|
|
81
82
|
const assessmentText = getAssessmentText(assessment);
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
|
|
5
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
6
|
+
import { useSessionEventSubscriptions } from '../../../hooks/useSessionEventSubscriptions';
|
|
8
7
|
import type { components, Selector } from '@semiont/core';
|
|
9
8
|
import { getTextPositionSelector, getTargetSelector } from '@semiont/core';
|
|
10
9
|
import { AssessmentEntry } from './AssessmentEntry';
|
|
@@ -40,6 +39,10 @@ function getSelectorDisplayText(selector: Selector | Selector[]): string | null
|
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
interface AssessmentPanelProps {
|
|
42
|
+
/** Session carrying the client and event bus; null renders inert. */
|
|
43
|
+
session: SemiontSession | null;
|
|
44
|
+
/** The '@id' of the panel's resource — stamped as `source` on mark:submit (multi-viewer routing). */
|
|
45
|
+
resourceId: string;
|
|
43
46
|
annotations: Annotation[];
|
|
44
47
|
pendingAnnotation: PendingAnnotation | null;
|
|
45
48
|
isAssisting?: boolean;
|
|
@@ -61,6 +64,8 @@ interface AssessmentPanelProps {
|
|
|
61
64
|
* @subscribes browse:click - Annotation clicked. Payload: { annotationId: string }
|
|
62
65
|
*/
|
|
63
66
|
export function AssessmentPanel({
|
|
67
|
+
session,
|
|
68
|
+
resourceId,
|
|
64
69
|
annotations,
|
|
65
70
|
pendingAnnotation,
|
|
66
71
|
isAssisting = false,
|
|
@@ -73,7 +78,6 @@ export function AssessmentPanel({
|
|
|
73
78
|
hoveredAnnotationId,
|
|
74
79
|
}: AssessmentPanelProps) {
|
|
75
80
|
const t = useTranslations('AssessmentPanel');
|
|
76
|
-
const session = useObservable(useSemiont().activeSession$);
|
|
77
81
|
const [newAssessmentText, setNewAssessmentText] = useState('');
|
|
78
82
|
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
|
|
79
83
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -151,6 +155,7 @@ export function AssessmentPanel({
|
|
|
151
155
|
: undefined;
|
|
152
156
|
|
|
153
157
|
session?.client.mark.submit({
|
|
158
|
+
source: resourceId,
|
|
154
159
|
motivation: 'assessing',
|
|
155
160
|
selector: pendingAnnotation.selector,
|
|
156
161
|
...(body !== undefined ? { body } : {}),
|
|
@@ -181,7 +186,7 @@ export function AssessmentPanel({
|
|
|
181
186
|
}, []);
|
|
182
187
|
|
|
183
188
|
// Subscribe to click events - update focused state
|
|
184
|
-
|
|
189
|
+
useSessionEventSubscriptions(session, {
|
|
185
190
|
'browse:click': handleAnnotationClick,
|
|
186
191
|
});
|
|
187
192
|
|
|
@@ -243,6 +248,7 @@ export function AssessmentPanel({
|
|
|
243
248
|
{/* Assist Section - only in Annotate mode and for text resources */}
|
|
244
249
|
{annotateMode && (
|
|
245
250
|
<AssistSection
|
|
251
|
+
session={session}
|
|
246
252
|
annotationType="assessment"
|
|
247
253
|
isAssisting={isAssisting}
|
|
248
254
|
locale={locale}
|
|
@@ -260,6 +266,7 @@ export function AssessmentPanel({
|
|
|
260
266
|
) : (
|
|
261
267
|
sortedAnnotations.map((assessment) => (
|
|
262
268
|
<AssessmentEntry
|
|
269
|
+
session={session}
|
|
263
270
|
key={assessment.id}
|
|
264
271
|
assessment={assessment}
|
|
265
272
|
isFocused={assessment.id === focusedAnnotationId}
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
-
import {
|
|
6
|
-
import { useObservable } from '../../../hooks/useObservable';
|
|
5
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
7
6
|
import type { Motivation, components } from '@semiont/core';
|
|
8
7
|
import './AssistSection.css';
|
|
9
8
|
|
|
10
9
|
type JobProgress = components['schemas']['JobProgress'];
|
|
11
10
|
|
|
12
11
|
interface AssistSectionProps {
|
|
12
|
+
/** Session carrying the client and event bus; null renders inert. */
|
|
13
|
+
session: SemiontSession | null;
|
|
13
14
|
annotationType: 'highlight' | 'assessment' | 'comment';
|
|
14
15
|
isAssisting: boolean;
|
|
15
16
|
/** User UI locale — written into the annotation body's `language` field for comment/assessment. */
|
|
@@ -34,6 +35,7 @@ interface AssistSectionProps {
|
|
|
34
35
|
* @emits mark:progress-dismiss - Dismiss the annotation progress display
|
|
35
36
|
*/
|
|
36
37
|
export function AssistSection({
|
|
38
|
+
session,
|
|
37
39
|
annotationType,
|
|
38
40
|
isAssisting,
|
|
39
41
|
locale,
|
|
@@ -45,7 +47,6 @@ export function AssistSection({
|
|
|
45
47
|
annotationType === 'assessment' ? 'AssessmentPanel' :
|
|
46
48
|
'CommentsPanel';
|
|
47
49
|
const t = useTranslations(panelName);
|
|
48
|
-
const session = useObservable(useSemiont().activeSession$);
|
|
49
50
|
const [instructions, setInstructions] = useState('');
|
|
50
51
|
type ToneValue = 'scholarly' | 'explanatory' | 'conversational' | 'technical' | 'analytical' | 'critical' | 'balanced' | 'constructive' | '';
|
|
51
52
|
const [tone, setTone] = useState<ToneValue>('');
|
|
@@ -4,12 +4,13 @@ import { useState, useEffect, useRef, useImperativeHandle, type Ref } from 'reac
|
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
5
|
import type { Annotation } from '@semiont/core';
|
|
6
6
|
import { getAnnotationExactText, getCommentText } from '@semiont/core';
|
|
7
|
-
import {
|
|
8
|
-
import { useObservable } from '../../../hooks/useObservable';
|
|
7
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
9
8
|
import { useHoverEmitter } from '../../../hooks/useHoverEmitter';
|
|
10
9
|
import { renderAgentLabel } from './agent-label';
|
|
11
10
|
|
|
12
11
|
interface CommentEntryProps {
|
|
12
|
+
/** Session for interaction routing (browse.click etc.); the panel threads it. */
|
|
13
|
+
session: SemiontSession | null;
|
|
13
14
|
comment: Annotation;
|
|
14
15
|
isFocused: boolean;
|
|
15
16
|
isHovered?: boolean;
|
|
@@ -35,6 +36,7 @@ function formatRelativeTime(isoString: string): string {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export function CommentEntry({
|
|
39
|
+
session,
|
|
38
40
|
comment,
|
|
39
41
|
isFocused,
|
|
40
42
|
isHovered = false,
|
|
@@ -42,8 +44,7 @@ export function CommentEntry({
|
|
|
42
44
|
ref,
|
|
43
45
|
}: CommentEntryProps) {
|
|
44
46
|
const t = useTranslations('CommentsPanel');
|
|
45
|
-
const
|
|
46
|
-
const hoverProps = useHoverEmitter(comment.id);
|
|
47
|
+
const hoverProps = useHoverEmitter(session, comment.id);
|
|
47
48
|
const [isEditing, setIsEditing] = useState(false);
|
|
48
49
|
const [editText, setEditText] = useState('');
|
|
49
50
|
const internalRef = useRef<HTMLDivElement>(null);
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
|
|
5
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
6
|
+
import { useSessionEventSubscriptions } from '../../../hooks/useSessionEventSubscriptions';
|
|
8
7
|
import type { components, Selector } from '@semiont/core';
|
|
9
8
|
import { getTextPositionSelector, getTargetSelector } from '@semiont/core';
|
|
10
9
|
import { CommentEntry } from './CommentEntry';
|
|
@@ -40,6 +39,10 @@ function getSelectorDisplayText(selector: Selector | Selector[]): string | null
|
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
interface CommentsPanelProps {
|
|
42
|
+
/** Session carrying the client and event bus; null renders inert. */
|
|
43
|
+
session: SemiontSession | null;
|
|
44
|
+
/** The '@id' of the panel's resource — stamped as `source` on mark:submit (multi-viewer routing). */
|
|
45
|
+
resourceId: string;
|
|
43
46
|
annotations: Annotation[];
|
|
44
47
|
pendingAnnotation: PendingAnnotation | null;
|
|
45
48
|
annotateMode?: boolean;
|
|
@@ -61,6 +64,8 @@ interface CommentsPanelProps {
|
|
|
61
64
|
* @subscribes browse:click - Annotation clicked. Payload: { annotationId: string }
|
|
62
65
|
*/
|
|
63
66
|
export function CommentsPanel({
|
|
67
|
+
session,
|
|
68
|
+
resourceId,
|
|
64
69
|
annotations,
|
|
65
70
|
pendingAnnotation,
|
|
66
71
|
annotateMode = true,
|
|
@@ -73,7 +78,6 @@ export function CommentsPanel({
|
|
|
73
78
|
hoveredAnnotationId,
|
|
74
79
|
}: CommentsPanelProps) {
|
|
75
80
|
const t = useTranslations('CommentsPanel');
|
|
76
|
-
const session = useObservable(useSemiont().activeSession$);
|
|
77
81
|
const [newCommentText, setNewCommentText] = useState('');
|
|
78
82
|
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
|
|
79
83
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -164,13 +168,14 @@ export function CommentsPanel({
|
|
|
164
168
|
setTimeout(() => setFocusedAnnotationId(null), 3000);
|
|
165
169
|
}, []);
|
|
166
170
|
|
|
167
|
-
|
|
171
|
+
useSessionEventSubscriptions(session, {
|
|
168
172
|
'browse:click': handleAnnotationClick,
|
|
169
173
|
});
|
|
170
174
|
|
|
171
175
|
const handleSaveNewComment = () => {
|
|
172
176
|
if (newCommentText.trim() && pendingAnnotation) {
|
|
173
177
|
session?.client.mark.submit({
|
|
178
|
+
source: resourceId,
|
|
174
179
|
motivation: 'commenting',
|
|
175
180
|
selector: pendingAnnotation.selector,
|
|
176
181
|
body: [{ type: 'TextualBody', value: newCommentText, purpose: 'commenting' }],
|
|
@@ -253,6 +258,7 @@ export function CommentsPanel({
|
|
|
253
258
|
{/* Assist Section - only in Annotate mode and for text resources */}
|
|
254
259
|
{annotateMode && (
|
|
255
260
|
<AssistSection
|
|
261
|
+
session={session}
|
|
256
262
|
annotationType="comment"
|
|
257
263
|
isAssisting={isAssisting}
|
|
258
264
|
locale={locale}
|
|
@@ -270,6 +276,7 @@ export function CommentsPanel({
|
|
|
270
276
|
) : (
|
|
271
277
|
sortedAnnotations.map((comment) => (
|
|
272
278
|
<CommentEntry
|
|
279
|
+
session={session}
|
|
273
280
|
key={comment.id}
|
|
274
281
|
comment={comment}
|
|
275
282
|
isFocused={comment.id === focusedAnnotationId}
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
import type { Ref } from 'react';
|
|
4
4
|
import type { Annotation } from '@semiont/core';
|
|
5
5
|
import { getAnnotationExactText } from '@semiont/core';
|
|
6
|
-
import {
|
|
7
|
-
import { useObservable } from '../../../hooks/useObservable';
|
|
6
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
8
7
|
import { useHoverEmitter } from '../../../hooks/useHoverEmitter';
|
|
9
8
|
import { renderAgentLabel } from './agent-label';
|
|
10
9
|
|
|
11
10
|
interface HighlightEntryProps {
|
|
11
|
+
/** Session for interaction routing (browse.click etc.); the panel threads it. */
|
|
12
|
+
session: SemiontSession | null;
|
|
12
13
|
highlight: Annotation;
|
|
13
14
|
isFocused: boolean;
|
|
14
15
|
isHovered?: boolean;
|
|
@@ -33,13 +34,13 @@ function formatRelativeTime(isoString: string): string {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export function HighlightEntry({
|
|
37
|
+
session,
|
|
36
38
|
highlight,
|
|
37
39
|
isFocused,
|
|
38
40
|
isHovered = false,
|
|
39
41
|
ref,
|
|
40
42
|
}: HighlightEntryProps) {
|
|
41
|
-
const
|
|
42
|
-
const hoverProps = useHoverEmitter(highlight.id);
|
|
43
|
+
const hoverProps = useHoverEmitter(session, highlight.id);
|
|
43
44
|
|
|
44
45
|
const selectedText = getAnnotationExactText(highlight);
|
|
45
46
|
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
|
|
5
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
6
|
+
import { useSessionEventSubscriptions } from '../../../hooks/useSessionEventSubscriptions';
|
|
8
7
|
import type { components, Selector } from '@semiont/core';
|
|
9
8
|
import { getTextPositionSelector, getTargetSelector } from '@semiont/core';
|
|
10
9
|
import { HighlightEntry } from './HighlightEntry';
|
|
@@ -23,6 +22,10 @@ interface PendingAnnotation {
|
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
interface HighlightPanelProps {
|
|
25
|
+
/** Session carrying the client and event bus; null renders inert. */
|
|
26
|
+
session: SemiontSession | null;
|
|
27
|
+
/** The '@id' of the panel's resource — stamped as `source` on mark:submit (multi-viewer routing). */
|
|
28
|
+
resourceId: string;
|
|
26
29
|
annotations: Annotation[];
|
|
27
30
|
pendingAnnotation: PendingAnnotation | null;
|
|
28
31
|
isAssisting?: boolean;
|
|
@@ -42,6 +45,8 @@ interface HighlightPanelProps {
|
|
|
42
45
|
* @subscribes browse:click - Annotation clicked. Payload: { annotationId: string }
|
|
43
46
|
*/
|
|
44
47
|
export function HighlightPanel({
|
|
48
|
+
session,
|
|
49
|
+
resourceId,
|
|
45
50
|
annotations,
|
|
46
51
|
pendingAnnotation,
|
|
47
52
|
isAssisting = false,
|
|
@@ -54,7 +59,6 @@ export function HighlightPanel({
|
|
|
54
59
|
}: HighlightPanelProps) {
|
|
55
60
|
|
|
56
61
|
const t = useTranslations('HighlightPanel');
|
|
57
|
-
const session = useObservable(useSemiont().activeSession$);
|
|
58
62
|
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
|
|
59
63
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
60
64
|
|
|
@@ -125,7 +129,7 @@ export function HighlightPanel({
|
|
|
125
129
|
setTimeout(() => setFocusedAnnotationId(null), 3000);
|
|
126
130
|
}, []);
|
|
127
131
|
|
|
128
|
-
|
|
132
|
+
useSessionEventSubscriptions(session, {
|
|
129
133
|
'browse:click': handleAnnotationClick,
|
|
130
134
|
});
|
|
131
135
|
|
|
@@ -136,6 +140,7 @@ export function HighlightPanel({
|
|
|
136
140
|
useEffect(() => {
|
|
137
141
|
if (pendingAnnotation && pendingAnnotation.motivation === 'highlighting') {
|
|
138
142
|
session?.client.mark.submit({
|
|
143
|
+
source: resourceId,
|
|
139
144
|
motivation: 'highlighting',
|
|
140
145
|
selector: pendingAnnotation.selector,
|
|
141
146
|
});
|
|
@@ -151,6 +156,7 @@ export function HighlightPanel({
|
|
|
151
156
|
{/* Assist Section - only in Annotate mode and for text resources */}
|
|
152
157
|
{annotateMode && (
|
|
153
158
|
<AssistSection
|
|
159
|
+
session={session}
|
|
154
160
|
annotationType="highlight"
|
|
155
161
|
isAssisting={isAssisting}
|
|
156
162
|
progress={progress}
|
|
@@ -167,6 +173,7 @@ export function HighlightPanel({
|
|
|
167
173
|
) : (
|
|
168
174
|
sortedAnnotations.map((highlight) => (
|
|
169
175
|
<HighlightEntry
|
|
176
|
+
session={session}
|
|
170
177
|
key={highlight.id}
|
|
171
178
|
highlight={highlight}
|
|
172
179
|
isFocused={highlight.id === focusedAnnotationId}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import type { Ref } from 'react';
|
|
4
|
-
import type { RouteBuilder } from '../../../contexts/RoutingContext';
|
|
5
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
6
5
|
import type { Annotation } from '@semiont/core';
|
|
7
6
|
import { resourceId } from '@semiont/core';
|
|
8
7
|
import { getAnnotationExactText, isBodyResolved, getBodySource, getFragmentSelector, getSvgSelector, getTargetSelector } from '@semiont/core';
|
|
9
8
|
import { getEntityTypes } from '@semiont/ontology';
|
|
10
9
|
import { getResourceIcon } from '../../../lib/resource-utils';
|
|
11
|
-
import {
|
|
12
|
-
import { useObservable } from '../../../hooks/useObservable';
|
|
10
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
13
11
|
import { renderAgentLabel } from './agent-label';
|
|
14
|
-
import { useObservableExternalNavigation } from '../../../hooks/useObservableBrowse';
|
|
15
12
|
import { useHoverEmitter } from '../../../hooks/useHoverEmitter';
|
|
16
13
|
|
|
17
14
|
// Extended annotation type with runtime properties added by backend enrichment
|
|
@@ -21,29 +18,31 @@ interface EnrichedAnnotation extends Annotation {
|
|
|
21
18
|
}
|
|
22
19
|
|
|
23
20
|
interface ReferenceEntryProps {
|
|
21
|
+
/** Session for interaction routing (browse.click etc.); the panel threads it. */
|
|
22
|
+
session: SemiontSession | null;
|
|
24
23
|
reference: Annotation;
|
|
25
24
|
isFocused: boolean;
|
|
26
25
|
isHovered?: boolean;
|
|
27
|
-
|
|
26
|
+
/** Host-owned navigation: called with the resolved resource id when the entry's link opens. */
|
|
27
|
+
onOpenResource?: (resourceId: string) => void;
|
|
28
28
|
annotateMode?: boolean;
|
|
29
29
|
isGenerating?: boolean;
|
|
30
30
|
ref?: Ref<HTMLDivElement>;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export function ReferenceEntry({
|
|
34
|
+
session,
|
|
34
35
|
reference,
|
|
35
36
|
isFocused,
|
|
36
37
|
isHovered = false,
|
|
37
|
-
|
|
38
|
+
onOpenResource,
|
|
38
39
|
annotateMode = true,
|
|
39
40
|
isGenerating = false,
|
|
40
41
|
ref,
|
|
41
42
|
}: ReferenceEntryProps) {
|
|
42
43
|
const t = useTranslations('ReferencesPanel');
|
|
43
|
-
const session = useObservable(useSemiont().activeSession$);
|
|
44
44
|
const semiont = session?.client;
|
|
45
|
-
const
|
|
46
|
-
const hoverProps = useHoverEmitter(reference.id);
|
|
45
|
+
const hoverProps = useHoverEmitter(session, reference.id);
|
|
47
46
|
|
|
48
47
|
const selectedText = getAnnotationExactText(reference) || '';
|
|
49
48
|
const isResolved = isBodyResolved(reference.body);
|
|
@@ -65,7 +64,7 @@ export function ReferenceEntry({
|
|
|
65
64
|
const handleOpen = () => {
|
|
66
65
|
if (resolvedResourceUri) {
|
|
67
66
|
// resolvedResourceUri is already a bare resource ID
|
|
68
|
-
|
|
67
|
+
onOpenResource?.(resolvedResourceUri);
|
|
69
68
|
}
|
|
70
69
|
};
|
|
71
70
|
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
|
|
5
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
6
|
+
import { useSessionEventSubscriptions } from '../../../hooks/useSessionEventSubscriptions';
|
|
8
7
|
import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
|
|
9
8
|
import { AnnotateReferencesProgressWidget } from '../../AnnotateReferencesProgressWidget';
|
|
10
9
|
import { ReferenceEntry } from './ReferenceEntry';
|
|
@@ -43,6 +42,12 @@ function getSelectorDisplayText(selector: Selector | Selector[]): string | null
|
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
interface Props {
|
|
45
|
+
/** Session carrying the client and event bus; null renders inert. */
|
|
46
|
+
session: SemiontSession | null;
|
|
47
|
+
/** Host-owned navigation: called with the resolved resource id when a reference entry opens. */
|
|
48
|
+
onOpenResource?: (resourceId: string) => void;
|
|
49
|
+
/** The '@id' of the panel's resource — stamped as `source` on mark:submit (multi-viewer routing). */
|
|
50
|
+
resourceId: string;
|
|
46
51
|
// Generic panel props
|
|
47
52
|
annotations?: Annotation[];
|
|
48
53
|
isAssisting: boolean;
|
|
@@ -76,6 +81,9 @@ interface Props {
|
|
|
76
81
|
* @subscribes browse:click - Annotation clicked. Payload: { annotationId: string }
|
|
77
82
|
*/
|
|
78
83
|
export function ReferencesPanel({
|
|
84
|
+
session,
|
|
85
|
+
onOpenResource,
|
|
86
|
+
resourceId,
|
|
79
87
|
annotations = [],
|
|
80
88
|
isAssisting,
|
|
81
89
|
progress,
|
|
@@ -94,7 +102,6 @@ export function ReferencesPanel({
|
|
|
94
102
|
sourceLanguage,
|
|
95
103
|
}: Props) {
|
|
96
104
|
const t = useTranslations('ReferencesPanel');
|
|
97
|
-
const session = useObservable(useSemiont().activeSession$);
|
|
98
105
|
const [selectedEntityTypes, setSelectedEntityTypes] = useState<string[]>([]);
|
|
99
106
|
const [lastAnnotationLog, setLastDetectionLog] = useState<Array<{ entityType: string; foundCount: number }> | null>(null);
|
|
100
107
|
const [pendingEntityTypes, setPendingEntityTypes] = useState<string[]>([]);
|
|
@@ -203,7 +210,7 @@ export function ReferencesPanel({
|
|
|
203
210
|
setTimeout(() => setFocusedAnnotationId(null), 3000);
|
|
204
211
|
}, []);
|
|
205
212
|
|
|
206
|
-
|
|
213
|
+
useSessionEventSubscriptions(session, {
|
|
207
214
|
'browse:click': handleAnnotationClick,
|
|
208
215
|
});
|
|
209
216
|
|
|
@@ -255,6 +262,7 @@ export function ReferencesPanel({
|
|
|
255
262
|
if (pendingAnnotation) {
|
|
256
263
|
const entityType = pendingEntityTypes.join(',') || undefined;
|
|
257
264
|
session?.client.mark.submit({
|
|
265
|
+
source: resourceId,
|
|
258
266
|
motivation: 'linking',
|
|
259
267
|
selector: pendingAnnotation.selector,
|
|
260
268
|
body: entityType ? [{ type: 'TextualBody', value: entityType, purpose: 'tagging' }] : [],
|
|
@@ -488,11 +496,12 @@ export function ReferencesPanel({
|
|
|
488
496
|
) : (
|
|
489
497
|
sortedAnnotations.map((reference) => (
|
|
490
498
|
<ReferenceEntry
|
|
499
|
+
session={session}
|
|
491
500
|
key={reference.id}
|
|
492
501
|
reference={reference}
|
|
493
502
|
isFocused={reference.id === focusedAnnotationId}
|
|
494
503
|
isHovered={reference.id === hoveredAnnotationId}
|
|
495
|
-
|
|
504
|
+
onOpenResource={onOpenResource}
|
|
496
505
|
annotateMode={annotateMode}
|
|
497
506
|
isGenerating={reference.id === generatingReferenceId}
|
|
498
507
|
ref={(el) => setEntryRef(reference.id, el)}
|