@semiont/react-ui 0.4.20 → 0.4.21
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 +8 -5
- package/dist/{PdfAnnotationCanvas.client-CHDCGQBR.mjs → PdfAnnotationCanvas.client-6ZGFEN2N.mjs} +9 -13
- package/dist/PdfAnnotationCanvas.client-6ZGFEN2N.mjs.map +1 -0
- package/dist/TranslationManager-9Xj3MIWQ.d.mts +16 -0
- package/dist/chunk-KEDFYI6N.mjs +7788 -0
- package/dist/chunk-KEDFYI6N.mjs.map +1 -0
- package/dist/index.d.mts +171 -1140
- package/dist/index.mjs +3263 -13644
- package/dist/index.mjs.map +1 -1
- package/dist/test-utils.d.mts +46 -21
- package/dist/test-utils.mjs +2499 -87
- package/dist/test-utils.mjs.map +1 -1
- package/package.json +1 -2
- package/src/components/AnnotateReferencesProgressWidget.tsx +21 -28
- package/src/components/CodeMirrorRenderer.tsx +9 -11
- package/src/components/StatusDisplay.tsx +42 -16
- package/src/components/Toolbar.tsx +4 -4
- package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +34 -20
- package/src/components/__tests__/StatusDisplay.test.tsx +47 -64
- package/src/components/__tests__/Toolbar.test.tsx +4 -4
- package/src/components/annotation/AnnotateToolbar.tsx +8 -7
- package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +31 -77
- package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +0 -1
- package/src/components/image-annotation/AnnotationOverlay.tsx +12 -13
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +7 -7
- package/src/components/modals/PermissionDeniedModal.tsx +11 -11
- package/src/components/modals/ReferenceWizardModal.tsx +14 -18
- package/src/components/modals/ResourceSearchModal.tsx +10 -6
- package/src/components/modals/SearchModal.tsx +10 -6
- package/src/components/modals/SessionExpiredModal.tsx +11 -11
- package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +7 -7
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +10 -8
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +10 -7
- package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +5 -5
- package/src/components/navigation/CollapsibleResourceNavigation.tsx +10 -10
- package/src/components/navigation/ObservableLink.tsx +6 -6
- package/src/components/navigation/SimpleNavigation.tsx +4 -4
- package/src/components/navigation/__tests__/ObservableLink.test.tsx +4 -4
- package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +4 -4
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +9 -11
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +0 -1
- package/src/components/resource/AnnotateView.tsx +7 -6
- package/src/components/resource/AnnotationHistory.tsx +9 -12
- package/src/components/resource/BrowseView.tsx +8 -7
- package/src/components/resource/ResourceViewer.tsx +17 -25
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +54 -192
- package/src/components/resource/__tests__/BrowseView.test.tsx +34 -83
- package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +40 -31
- package/src/components/resource/panels/AssessmentEntry.tsx +5 -4
- package/src/components/resource/panels/AssessmentPanel.tsx +19 -15
- package/src/components/resource/panels/AssistSection.tsx +11 -13
- package/src/components/resource/panels/CollaborationPanel.tsx +29 -7
- package/src/components/resource/panels/CommentEntry.tsx +5 -4
- package/src/components/resource/panels/CommentsPanel.tsx +9 -11
- package/src/components/resource/panels/HighlightEntry.tsx +5 -4
- package/src/components/resource/panels/HighlightPanel.tsx +10 -11
- package/src/components/resource/panels/ReferenceEntry.tsx +8 -8
- package/src/components/resource/panels/ReferencesPanel.tsx +13 -12
- package/src/components/resource/panels/ResourceInfoPanel.tsx +7 -6
- package/src/components/resource/panels/TagEntry.tsx +5 -4
- package/src/components/resource/panels/TaggingPanel.tsx +10 -16
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +3 -2
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +18 -52
- package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +51 -20
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +18 -56
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +4 -5
- package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +153 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +51 -106
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +15 -47
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +15 -47
- package/src/components/settings/SettingsPanel.tsx +8 -8
- package/src/components/settings/__tests__/SettingsPanel.test.tsx +12 -12
- package/src/features/admin-devops/components/AdminDevOpsPage.tsx +1 -1
- package/src/features/admin-exchange/components/AdminExchangePage.tsx +1 -1
- package/src/features/admin-exchange/components/ImportCard.tsx +2 -6
- package/src/features/admin-security/components/AdminSecurityPage.tsx +1 -1
- package/src/features/admin-users/components/AdminUsersPage.tsx +1 -1
- package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -1
- package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -1
- package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -1
- package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +1 -1
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +5 -3
- package/src/features/resource-compose/components/ResourceComposePage.tsx +5 -22
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +4 -3
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +1 -1
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +38 -45
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +123 -192
- package/dist/KnowledgeBaseSessionContext-BNNunwzO.d.mts +0 -175
- package/dist/PdfAnnotationCanvas.client-CHDCGQBR.mjs.map +0 -1
- package/dist/chunk-OZICDVH7.mjs +0 -62
- package/dist/chunk-OZICDVH7.mjs.map +0 -1
- package/dist/chunk-R4CCMFJH.mjs +0 -877
- package/dist/chunk-R4CCMFJH.mjs.map +0 -1
- package/dist/chunk-VN5NY4SN.mjs +0 -200
- package/dist/chunk-VN5NY4SN.mjs.map +0 -1
- package/src/components/modals/ProposeEntitiesModal.tsx +0 -179
- package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +0 -129
- package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +0 -323
- package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +0 -245
- package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +0 -303
- package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +0 -150
- package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +0 -243
- package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +0 -383
- package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +0 -299
- package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +0 -186
- package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +0 -429
- package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +0 -348
|
@@ -66,15 +66,15 @@ describe('Toolbar', () => {
|
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
describe('event emission', () => {
|
|
69
|
-
it('emits
|
|
69
|
+
it('emits panel:toggle with panel name on click', () => {
|
|
70
70
|
const handler = vi.fn();
|
|
71
71
|
|
|
72
|
-
const {
|
|
72
|
+
const { shellBus } = renderWithProviders(
|
|
73
73
|
<Toolbar context="document" activePanel={null} />,
|
|
74
|
-
{
|
|
74
|
+
{ returnShellBus: true }
|
|
75
75
|
);
|
|
76
76
|
|
|
77
|
-
const subscription =
|
|
77
|
+
const subscription = shellBus!.get('panel:toggle').subscribe(handler);
|
|
78
78
|
|
|
79
79
|
fireEvent.click(screen.getByLabelText('Toolbar.resourceInfo'));
|
|
80
80
|
expect(handler).toHaveBeenCalledWith({ panel: 'info' });
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useRef, useEffect } from 'react';
|
|
4
4
|
import { useTranslations } from '../../contexts/TranslationContext';
|
|
5
|
-
import {
|
|
5
|
+
import { useSemiont } from '../../session/SemiontProvider';
|
|
6
|
+
import { useObservable } from '../../hooks/useObservable';
|
|
6
7
|
import { getSupportedShapes } from '../../lib/media-shapes';
|
|
7
8
|
import type { Annotator } from '../../lib/annotation-registry';
|
|
8
9
|
import './annotations.css';
|
|
@@ -119,7 +120,7 @@ export function AnnotateToolbar({
|
|
|
119
120
|
annotators
|
|
120
121
|
}: AnnotateToolbarProps) {
|
|
121
122
|
const t = useTranslations('AnnotateToolbar');
|
|
122
|
-
const
|
|
123
|
+
const session = useObservable(useSemiont().activeSession$);
|
|
123
124
|
|
|
124
125
|
// Helper to get emoji from annotators by motivation (with fallback for safety)
|
|
125
126
|
const getMotivationEmoji = (motivation: SelectionMotivation): string => {
|
|
@@ -188,9 +189,9 @@ export function AnnotateToolbar({
|
|
|
188
189
|
const handleSelectionClick = (motivation: SelectionMotivation | null) => {
|
|
189
190
|
// If null is clicked, always deselect. Otherwise toggle.
|
|
190
191
|
if (motivation === null) {
|
|
191
|
-
|
|
192
|
+
session?.client.emit('mark:selection-changed', { motivation: null });
|
|
192
193
|
} else {
|
|
193
|
-
|
|
194
|
+
session?.client.emit('mark:selection-changed', {
|
|
194
195
|
motivation: selectedMotivation === motivation ? null : motivation
|
|
195
196
|
});
|
|
196
197
|
}
|
|
@@ -200,21 +201,21 @@ export function AnnotateToolbar({
|
|
|
200
201
|
};
|
|
201
202
|
|
|
202
203
|
const handleClickClick = (action: ClickAction) => {
|
|
203
|
-
|
|
204
|
+
session?.client.emit('mark:click-changed', { action });
|
|
204
205
|
// Close dropdown after selection
|
|
205
206
|
setClickPinned(false);
|
|
206
207
|
setClickHovered(false);
|
|
207
208
|
};
|
|
208
209
|
|
|
209
210
|
const handleShapeClick = (shape: ShapeType) => {
|
|
210
|
-
|
|
211
|
+
session?.client.emit('mark:shape-changed', { shape });
|
|
211
212
|
// Close dropdown after selection
|
|
212
213
|
setShapePinned(false);
|
|
213
214
|
setShapeHovered(false);
|
|
214
215
|
};
|
|
215
216
|
|
|
216
217
|
const handleModeToggle = () => {
|
|
217
|
-
|
|
218
|
+
session?.client.emit('mark:mode-toggled', undefined);
|
|
218
219
|
setModePinned(false);
|
|
219
220
|
setModeHovered(false);
|
|
220
221
|
};
|
|
@@ -3,9 +3,10 @@ import { render, screen, fireEvent, waitFor, within } from '@testing-library/rea
|
|
|
3
3
|
import { vi, beforeEach, describe, it, expect } from 'vitest';
|
|
4
4
|
import { AnnotateToolbar, type SelectionMotivation, type ClickAction } from '../AnnotateToolbar';
|
|
5
5
|
import { ANNOTATORS } from '../../../lib/annotation-registry';
|
|
6
|
-
import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
|
|
7
6
|
import { TranslationProvider } from '../../../contexts/TranslationContext';
|
|
7
|
+
import { createTestSemiontWrapper } from '../../../test-utils';
|
|
8
8
|
import type { TranslationManager } from '../../../types/TranslationManager';
|
|
9
|
+
import type { EventBus } from '@semiont/core';
|
|
9
10
|
|
|
10
11
|
// Mock translations
|
|
11
12
|
const messages: Record<string, Record<string, string>> = {
|
|
@@ -43,68 +44,37 @@ interface TrackedEvent {
|
|
|
43
44
|
|
|
44
45
|
function createEventTracker() {
|
|
45
46
|
const events: TrackedEvent[] = [];
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const handlers: Array<() => void> = [];
|
|
52
|
-
|
|
53
|
-
// Track toolbar-related events
|
|
54
|
-
const trackEvent = (eventName: string) => (payload: any) => {
|
|
55
|
-
events.push({ event: eventName, payload });
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const toolbarEvents = [
|
|
47
|
+
return {
|
|
48
|
+
events,
|
|
49
|
+
clear: () => { events.length = 0; },
|
|
50
|
+
_attach(eventBus: EventBus) {
|
|
51
|
+
const trackedEvents = [
|
|
59
52
|
'mark:mode-toggled',
|
|
60
53
|
'mark:click-changed',
|
|
61
54
|
'mark:selection-changed',
|
|
62
55
|
'mark:shape-changed',
|
|
63
56
|
] as const;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
handlers.push(subscription);
|
|
57
|
+
trackedEvents.forEach((eventName) => {
|
|
58
|
+
eventBus.get(eventName).subscribe((payload: any) => {
|
|
59
|
+
events.push({ event: eventName, payload });
|
|
60
|
+
});
|
|
69
61
|
});
|
|
70
|
-
|
|
71
|
-
return () => {
|
|
72
|
-
handlers.forEach(sub => sub.unsubscribe());
|
|
73
|
-
};
|
|
74
|
-
}, [eventBus]);
|
|
75
|
-
|
|
76
|
-
return <>{children}</>;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
EventTrackingWrapper,
|
|
81
|
-
events,
|
|
82
|
-
clear: () => {
|
|
83
|
-
events.length = 0;
|
|
84
62
|
},
|
|
85
63
|
};
|
|
86
64
|
}
|
|
87
65
|
|
|
88
66
|
const renderWithIntl = (component: React.ReactElement, tracker?: ReturnType<typeof createEventTracker>) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<tracker.EventTrackingWrapper>
|
|
94
|
-
{component}
|
|
95
|
-
</tracker.EventTrackingWrapper>
|
|
96
|
-
</TranslationProvider>
|
|
97
|
-
</EventBusProvider>
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return render(
|
|
102
|
-
<EventBusProvider>
|
|
67
|
+
const { SemiontWrapper, eventBus } = createTestSemiontWrapper();
|
|
68
|
+
if (tracker) tracker._attach(eventBus);
|
|
69
|
+
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
70
|
+
<SemiontWrapper>
|
|
103
71
|
<TranslationProvider translationManager={translationManager}>
|
|
104
|
-
{
|
|
72
|
+
{children}
|
|
105
73
|
</TranslationProvider>
|
|
106
|
-
</
|
|
74
|
+
</SemiontWrapper>
|
|
107
75
|
);
|
|
76
|
+
const result = render(component, { wrapper: Wrapper });
|
|
77
|
+
return { ...result, eventBus };
|
|
108
78
|
};
|
|
109
79
|
|
|
110
80
|
describe('AnnotateToolbar', () => {
|
|
@@ -187,14 +157,10 @@ describe('AnnotateToolbar', () => {
|
|
|
187
157
|
expect(screen.getByText('Browse')).toBeInTheDocument();
|
|
188
158
|
|
|
189
159
|
rerender(
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
annotateMode={true}
|
|
195
|
-
/>
|
|
196
|
-
</TranslationProvider>
|
|
197
|
-
</EventBusProvider>
|
|
160
|
+
<AnnotateToolbar
|
|
161
|
+
{...defaultProps}
|
|
162
|
+
annotateMode={true}
|
|
163
|
+
/>
|
|
198
164
|
);
|
|
199
165
|
expect(screen.getByText('Annotate')).toBeInTheDocument();
|
|
200
166
|
});
|
|
@@ -292,16 +258,10 @@ describe('AnnotateToolbar', () => {
|
|
|
292
258
|
|
|
293
259
|
// Simulate mode change by rerendering with new mode
|
|
294
260
|
rerender(
|
|
295
|
-
<
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
{...defaultProps}
|
|
300
|
-
annotateMode={true}
|
|
301
|
-
/>
|
|
302
|
-
</tracker.EventTrackingWrapper>
|
|
303
|
-
</TranslationProvider>
|
|
304
|
-
</EventBusProvider>
|
|
261
|
+
<AnnotateToolbar
|
|
262
|
+
{...defaultProps}
|
|
263
|
+
annotateMode={true}
|
|
264
|
+
/>
|
|
305
265
|
);
|
|
306
266
|
|
|
307
267
|
// After mode change, the collapsed content should show "Annotate"
|
|
@@ -399,16 +359,10 @@ describe('AnnotateToolbar', () => {
|
|
|
399
359
|
|
|
400
360
|
// Simulate selection
|
|
401
361
|
rerender(
|
|
402
|
-
<
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
{...defaultProps}
|
|
407
|
-
selectedMotivation="highlighting"
|
|
408
|
-
/>
|
|
409
|
-
</tracker.EventTrackingWrapper>
|
|
410
|
-
</TranslationProvider>
|
|
411
|
-
</EventBusProvider>
|
|
362
|
+
<AnnotateToolbar
|
|
363
|
+
{...defaultProps}
|
|
364
|
+
selectedMotivation="highlighting"
|
|
365
|
+
/>
|
|
412
366
|
);
|
|
413
367
|
|
|
414
368
|
// Click again to deselect
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
4
|
import type { components } from '@semiont/core';
|
|
5
|
-
import { createHoverHandlers } from '
|
|
6
|
-
import { getSvgSelector, isHighlight, isReference, isAssessment, isComment, isTag, isBodyResolved, isResolvedReference } from '@semiont/api-client';
|
|
5
|
+
import { createHoverHandlers, getSvgSelector, isHighlight, isReference, isAssessment, isComment, isTag, isBodyResolved, isResolvedReference } from '@semiont/api-client';
|
|
7
6
|
import { parseSvgSelector } from '@semiont/api-client';
|
|
8
|
-
import type {
|
|
7
|
+
import type { SemiontSession } from '@semiont/api-client';
|
|
9
8
|
|
|
10
9
|
type Annotation = components['schemas']['Annotation'];
|
|
11
10
|
|
|
@@ -15,7 +14,7 @@ interface AnnotationOverlayProps {
|
|
|
15
14
|
imageHeight: number;
|
|
16
15
|
displayWidth: number;
|
|
17
16
|
displayHeight: number;
|
|
18
|
-
|
|
17
|
+
session?: SemiontSession | null | undefined;
|
|
19
18
|
hoveredAnnotationId?: string | null;
|
|
20
19
|
selectedAnnotationId?: string | null;
|
|
21
20
|
hoverDelayMs: number;
|
|
@@ -72,7 +71,7 @@ export function AnnotationOverlay({
|
|
|
72
71
|
imageHeight,
|
|
73
72
|
displayWidth,
|
|
74
73
|
displayHeight,
|
|
75
|
-
|
|
74
|
+
session,
|
|
76
75
|
hoveredAnnotationId,
|
|
77
76
|
selectedAnnotationId,
|
|
78
77
|
hoverDelayMs
|
|
@@ -81,8 +80,8 @@ export function AnnotationOverlay({
|
|
|
81
80
|
const scaleY = displayHeight / imageHeight;
|
|
82
81
|
|
|
83
82
|
const { handleMouseEnter, handleMouseLeave } = useMemo(
|
|
84
|
-
() => createHoverHandlers((annotationId) =>
|
|
85
|
-
[
|
|
83
|
+
() => createHoverHandlers((annotationId) => session?.client.emit('beckon:hover', { annotationId }), hoverDelayMs),
|
|
84
|
+
[session, hoverDelayMs]
|
|
86
85
|
);
|
|
87
86
|
|
|
88
87
|
return (
|
|
@@ -131,7 +130,7 @@ export function AnnotationOverlay({
|
|
|
131
130
|
className="semiont-annotation-overlay__shape"
|
|
132
131
|
data-hovered={isHovered ? 'true' : 'false'}
|
|
133
132
|
data-selected={isSelected ? 'true' : 'false'}
|
|
134
|
-
onClick={() =>
|
|
133
|
+
onClick={() => session?.client.emit('browse:click', { annotationId: annotation.id, motivation: annotation.motivation })}
|
|
135
134
|
onMouseEnter={() => handleMouseEnter(annotation.id)}
|
|
136
135
|
onMouseLeave={handleMouseLeave}
|
|
137
136
|
/>
|
|
@@ -144,7 +143,7 @@ export function AnnotationOverlay({
|
|
|
144
143
|
style={{ userSelect: 'none' }}
|
|
145
144
|
onClick={(e) => {
|
|
146
145
|
e.stopPropagation();
|
|
147
|
-
|
|
146
|
+
session?.client.emit('browse:click', { annotationId: annotation.id, motivation: annotation.motivation });
|
|
148
147
|
}}
|
|
149
148
|
onMouseEnter={() => handleMouseEnter(annotation.id)}
|
|
150
149
|
onMouseLeave={handleMouseLeave}
|
|
@@ -178,7 +177,7 @@ export function AnnotationOverlay({
|
|
|
178
177
|
className="semiont-annotation-overlay__shape"
|
|
179
178
|
data-hovered={isHovered ? 'true' : 'false'}
|
|
180
179
|
data-selected={isSelected ? 'true' : 'false'}
|
|
181
|
-
onClick={() =>
|
|
180
|
+
onClick={() => session?.client.emit('browse:click', { annotationId: annotation.id, motivation: annotation.motivation })}
|
|
182
181
|
onMouseEnter={() => handleMouseEnter(annotation.id)}
|
|
183
182
|
onMouseLeave={handleMouseLeave}
|
|
184
183
|
/>
|
|
@@ -191,7 +190,7 @@ export function AnnotationOverlay({
|
|
|
191
190
|
style={{ userSelect: 'none' }}
|
|
192
191
|
onClick={(e) => {
|
|
193
192
|
e.stopPropagation();
|
|
194
|
-
|
|
193
|
+
session?.client.emit('browse:click', { annotationId: annotation.id, motivation: annotation.motivation });
|
|
195
194
|
}}
|
|
196
195
|
onMouseEnter={() => handleMouseEnter(annotation.id)}
|
|
197
196
|
onMouseLeave={handleMouseLeave}
|
|
@@ -238,7 +237,7 @@ export function AnnotationOverlay({
|
|
|
238
237
|
className="semiont-annotation-overlay__shape"
|
|
239
238
|
data-hovered={isHovered ? 'true' : 'false'}
|
|
240
239
|
data-selected={isSelected ? 'true' : 'false'}
|
|
241
|
-
onClick={() =>
|
|
240
|
+
onClick={() => session?.client.emit('browse:click', { annotationId: annotation.id, motivation: annotation.motivation })}
|
|
242
241
|
onMouseEnter={() => handleMouseEnter(annotation.id)}
|
|
243
242
|
onMouseLeave={handleMouseLeave}
|
|
244
243
|
/>
|
|
@@ -251,7 +250,7 @@ export function AnnotationOverlay({
|
|
|
251
250
|
style={{ userSelect: 'none' }}
|
|
252
251
|
onClick={(e) => {
|
|
253
252
|
e.stopPropagation();
|
|
254
|
-
|
|
253
|
+
session?.client.emit('browse:click', { annotationId: annotation.id, motivation: annotation.motivation });
|
|
255
254
|
}}
|
|
256
255
|
onMouseEnter={() => handleMouseEnter(annotation.id)}
|
|
257
256
|
onMouseLeave={handleMouseLeave}
|
|
@@ -5,7 +5,7 @@ import type { components } from '@semiont/core';
|
|
|
5
5
|
import { createRectangleSvg, createCircleSvg, createPolygonSvg, scaleSvgToNative, parseSvgSelector, Point } from '@semiont/api-client';
|
|
6
6
|
import { AnnotationOverlay } from './AnnotationOverlay';
|
|
7
7
|
import type { SelectionMotivation } from '../annotation/AnnotateToolbar';
|
|
8
|
-
import type {
|
|
8
|
+
import type { SemiontSession } from '@semiont/api-client';
|
|
9
9
|
import { useHoverDelay } from '../../hooks/useHoverDelay';
|
|
10
10
|
|
|
11
11
|
type Annotation = components['schemas']['Annotation'];
|
|
@@ -40,7 +40,7 @@ interface SvgDrawingCanvasProps {
|
|
|
40
40
|
existingAnnotations?: Annotation[];
|
|
41
41
|
drawingMode: DrawingMode;
|
|
42
42
|
selectedMotivation?: SelectionMotivation | null;
|
|
43
|
-
|
|
43
|
+
session?: SemiontSession | null | undefined;
|
|
44
44
|
hoveredAnnotationId?: string | null;
|
|
45
45
|
selectedAnnotationId?: string | null;
|
|
46
46
|
hoverDelayMs?: number;
|
|
@@ -57,7 +57,7 @@ export function SvgDrawingCanvas({
|
|
|
57
57
|
existingAnnotations = [],
|
|
58
58
|
drawingMode,
|
|
59
59
|
selectedMotivation,
|
|
60
|
-
|
|
60
|
+
session,
|
|
61
61
|
hoveredAnnotationId,
|
|
62
62
|
selectedAnnotationId
|
|
63
63
|
}: SvgDrawingCanvasProps) {
|
|
@@ -212,7 +212,7 @@ export function SvgDrawingCanvas({
|
|
|
212
212
|
});
|
|
213
213
|
|
|
214
214
|
if (clickedAnnotation) {
|
|
215
|
-
|
|
215
|
+
session?.client.emit('browse:click', { annotationId: clickedAnnotation.id, motivation: clickedAnnotation.motivation });
|
|
216
216
|
setIsDrawing(false);
|
|
217
217
|
setStartPoint(null);
|
|
218
218
|
setCurrentPoint(null);
|
|
@@ -274,8 +274,8 @@ export function SvgDrawingCanvas({
|
|
|
274
274
|
);
|
|
275
275
|
|
|
276
276
|
// Emit annotation:requested event with SvgSelector
|
|
277
|
-
if (
|
|
278
|
-
|
|
277
|
+
if (session && selectedMotivation) {
|
|
278
|
+
session.client.emit('mark:requested', {
|
|
279
279
|
selector: {
|
|
280
280
|
type: 'SvgSelector',
|
|
281
281
|
value: nativeSvg
|
|
@@ -338,7 +338,7 @@ export function SvgDrawingCanvas({
|
|
|
338
338
|
displayWidth={displayDimensions.width}
|
|
339
339
|
displayHeight={displayDimensions.height}
|
|
340
340
|
hoverDelayMs={hoverDelayMs}
|
|
341
|
-
{...(
|
|
341
|
+
{...(session && { session })}
|
|
342
342
|
{...(hoveredAnnotationId !== undefined && { hoveredAnnotationId })}
|
|
343
343
|
{...(selectedAnnotationId !== undefined && { selectedAnnotationId })}
|
|
344
344
|
/>
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
|
4
|
-
import {
|
|
4
|
+
import { useSemiont } from '../../session/SemiontProvider';
|
|
5
|
+
import { useObservable } from '../../hooks/useObservable';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Modal that surfaces when a 403 forbidden error is reported via
|
|
8
9
|
* `notifyPermissionDenied` (called from QueryCache.onError).
|
|
9
10
|
*
|
|
10
|
-
* Reads `permissionDeniedAt
|
|
11
|
-
*
|
|
12
|
-
* dismisses the modal.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Reads `permissionDeniedAt$` and `permissionDeniedMessage$` from the
|
|
12
|
+
* active `FrontendSessionSignals`. The signals instance clears the
|
|
13
|
+
* flag when the user dismisses the modal. Modal state lives on
|
|
14
|
+
* signals (not the session itself) so headless sessions
|
|
15
|
+
* (workers/CLIs) don't carry dead observables.
|
|
15
16
|
*/
|
|
16
17
|
export function PermissionDeniedModal() {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} = useKnowledgeBaseSession();
|
|
18
|
+
const signals = useObservable(useSemiont().activeSignals$);
|
|
19
|
+
const permissionDeniedAt = useObservable(signals?.permissionDeniedAt$) ?? null;
|
|
20
|
+
const permissionDeniedMessage = useObservable(signals?.permissionDeniedMessage$) ?? null;
|
|
21
|
+
const acknowledgePermissionDenied = () => signals?.acknowledgePermissionDenied();
|
|
22
22
|
const showModal = permissionDeniedAt !== null;
|
|
23
23
|
const message = permissionDeniedMessage ?? 'You do not have permission to perform this action.';
|
|
24
24
|
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback } from 'react';
|
|
4
4
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
|
5
|
-
import type { GatheredContext
|
|
5
|
+
import type { GatheredContext } from '@semiont/core';
|
|
6
|
+
import { useSemiont } from '../../session/SemiontProvider';
|
|
7
|
+
import { useObservable } from '../../hooks/useObservable';
|
|
8
|
+
import { useEventSubscription } from '../../contexts/useEventSubscription';
|
|
6
9
|
import { GatherContextStep } from './GatherContextStep';
|
|
7
10
|
import { ConfigureGenerationStep } from './ConfigureGenerationStep';
|
|
8
11
|
import type { GenerationConfig } from './ConfigureGenerationStep';
|
|
@@ -34,8 +37,6 @@ export interface ReferenceWizardModalProps {
|
|
|
34
37
|
context: GatheredContext | null;
|
|
35
38
|
contextLoading: boolean;
|
|
36
39
|
contextError: Error | null;
|
|
37
|
-
/** Event bus for emitting downstream events */
|
|
38
|
-
eventBus: EventBus;
|
|
39
40
|
/** Callbacks */
|
|
40
41
|
onGenerateSubmit: (referenceId: string, config: GenerationConfig) => void;
|
|
41
42
|
onLinkResource: (referenceId: string, targetResourceId: string) => void;
|
|
@@ -91,12 +92,12 @@ export function ReferenceWizardModal({
|
|
|
91
92
|
context,
|
|
92
93
|
contextLoading,
|
|
93
94
|
contextError,
|
|
94
|
-
eventBus,
|
|
95
95
|
onGenerateSubmit,
|
|
96
96
|
onLinkResource,
|
|
97
97
|
onComposeNavigate,
|
|
98
98
|
translations: t,
|
|
99
99
|
}: ReferenceWizardModalProps) {
|
|
100
|
+
const session = useObservable(useSemiont().activeSession$);
|
|
100
101
|
const [wizardStep, setWizardStep] = useState<WizardStep>({ step: 'gather' });
|
|
101
102
|
const [isSearching, setIsSearching] = useState(false);
|
|
102
103
|
const [userHint, setUserHint] = useState('');
|
|
@@ -110,19 +111,14 @@ export function ReferenceWizardModal({
|
|
|
110
111
|
}
|
|
111
112
|
}, [isOpen]);
|
|
112
113
|
|
|
113
|
-
// Subscribe to search results
|
|
114
|
-
|
|
114
|
+
// Subscribe to search results (only react while open and for the current annotation)
|
|
115
|
+
useEventSubscription('match:search-results', (event) => {
|
|
115
116
|
if (!isOpen) return;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
return () => subscription.unsubscribe();
|
|
125
|
-
}, [isOpen, eventBus, annotationId]);
|
|
117
|
+
if (annotationId && event.referenceId === annotationId) {
|
|
118
|
+
setIsSearching(false);
|
|
119
|
+
setWizardStep({ step: 'search-results', results: event.response as ScoredResult[] });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
126
122
|
|
|
127
123
|
const handleBind = useCallback(() => {
|
|
128
124
|
setWizardStep({ step: 'configure-search' });
|
|
@@ -147,7 +143,7 @@ export function ReferenceWizardModal({
|
|
|
147
143
|
if (!annotationId || !context || !resourceId) return;
|
|
148
144
|
setIsSearching(true);
|
|
149
145
|
const contextWithHint = userHint ? { ...context, userHint } : context;
|
|
150
|
-
|
|
146
|
+
session?.client.emit('match:search-requested', {
|
|
151
147
|
correlationId: crypto.randomUUID(),
|
|
152
148
|
resourceId,
|
|
153
149
|
referenceId: annotationId,
|
|
@@ -156,7 +152,7 @@ export function ReferenceWizardModal({
|
|
|
156
152
|
useSemanticScoring: config.useSemanticScoring,
|
|
157
153
|
});
|
|
158
154
|
// Stay on configure-search until results arrive (subscription above handles transition)
|
|
159
|
-
}, [annotationId, resourceId, context,
|
|
155
|
+
}, [annotationId, resourceId, context, session, userHint]);
|
|
160
156
|
|
|
161
157
|
const handleGenerateSubmit = useCallback((config: GenerationConfig) => {
|
|
162
158
|
if (!annotationId) return;
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
4
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
|
5
5
|
import { map } from 'rxjs/operators';
|
|
6
6
|
import type { components } from '@semiont/core';
|
|
7
|
-
import { getResourceId, getPrimaryRepresentation } from '@semiont/api-client';
|
|
8
|
-
import {
|
|
7
|
+
import { getResourceId, getPrimaryRepresentation, createSearchPipeline } from '@semiont/api-client';
|
|
8
|
+
import { useSemiont } from '../../session/SemiontProvider';
|
|
9
9
|
import { useObservable } from '../../hooks/useObservable';
|
|
10
10
|
import { useSearchAnnouncements } from '../../hooks/useSearchAnnouncements';
|
|
11
|
-
import { createSearchPipeline } from '../../lib/search-pipeline';
|
|
12
11
|
|
|
13
12
|
type ResourceDescriptor = components['schemas']['ResourceDescriptor'];
|
|
14
13
|
|
|
@@ -56,7 +55,12 @@ export function ResourceSearchModal({
|
|
|
56
55
|
translations = {}
|
|
57
56
|
}: ResourceSearchModalProps) {
|
|
58
57
|
const { announceSearchResults, announceSearching, announceNavigation } = useSearchAnnouncements();
|
|
59
|
-
const semiont =
|
|
58
|
+
const semiont = useObservable(useSemiont().activeSession$)?.client;
|
|
59
|
+
// Pipeline factory captures `semiont` once via useState; if semiont is
|
|
60
|
+
// still loading at first render the captured value would be undefined.
|
|
61
|
+
// Route through a ref so the fetch closure reads the latest client.
|
|
62
|
+
const semiontRef = useRef(semiont);
|
|
63
|
+
semiontRef.current = semiont;
|
|
60
64
|
|
|
61
65
|
const t = {
|
|
62
66
|
title: translations.title || 'Search Resources',
|
|
@@ -70,7 +74,7 @@ export function ResourceSearchModal({
|
|
|
70
74
|
const [pipeline] = useState(() =>
|
|
71
75
|
createSearchPipeline<SearchResult>(
|
|
72
76
|
(q) =>
|
|
73
|
-
|
|
77
|
+
semiontRef.current!.browse.resources({ search: q, limit: SEARCH_LIMIT }).pipe(
|
|
74
78
|
map((resources) => {
|
|
75
79
|
if (resources === undefined) return undefined;
|
|
76
80
|
return resources
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useState, useEffect } from 'react';
|
|
3
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
4
4
|
import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react';
|
|
5
5
|
import { map } from 'rxjs/operators';
|
|
6
|
-
import { getResourceId } from '@semiont/api-client';
|
|
6
|
+
import { getResourceId, createSearchPipeline } from '@semiont/api-client';
|
|
7
7
|
import { useSearchAnnouncements } from '../../hooks/useSearchAnnouncements';
|
|
8
|
-
import {
|
|
8
|
+
import { useSemiont } from '../../session/SemiontProvider';
|
|
9
9
|
import { useObservable } from '../../hooks/useObservable';
|
|
10
|
-
import { createSearchPipeline } from '../../lib/search-pipeline';
|
|
11
10
|
import './SearchModal.css';
|
|
12
11
|
|
|
13
12
|
const SEARCH_DEBOUNCE_MS = 300;
|
|
@@ -45,7 +44,12 @@ export function SearchModal({
|
|
|
45
44
|
translations = {}
|
|
46
45
|
}: SearchModalProps) {
|
|
47
46
|
const { announceSearchResults, announceSearching } = useSearchAnnouncements();
|
|
48
|
-
const semiont =
|
|
47
|
+
const semiont = useObservable(useSemiont().activeSession$)?.client;
|
|
48
|
+
// Pipeline factory captures `semiont` once via useState; if semiont is
|
|
49
|
+
// still loading at first render the captured value would be undefined.
|
|
50
|
+
// Route through a ref so the fetch closure reads the latest client.
|
|
51
|
+
const semiontRef = useRef(semiont);
|
|
52
|
+
semiontRef.current = semiont;
|
|
49
53
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
50
54
|
|
|
51
55
|
const t = {
|
|
@@ -66,7 +70,7 @@ export function SearchModal({
|
|
|
66
70
|
const [pipeline] = useState(() =>
|
|
67
71
|
createSearchPipeline<SearchResult>(
|
|
68
72
|
(q) =>
|
|
69
|
-
|
|
73
|
+
semiontRef.current!.browse.resources({ search: q, limit: SEARCH_LIMIT }).pipe(
|
|
70
74
|
map((resources) => {
|
|
71
75
|
if (resources === undefined) return undefined;
|
|
72
76
|
return resources
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
|
4
|
-
import {
|
|
4
|
+
import { useSemiont } from '../../session/SemiontProvider';
|
|
5
|
+
import { useObservable } from '../../hooks/useObservable';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Modal that surfaces when the active KB's session expires (a 401 from
|
|
8
|
-
* either the
|
|
9
|
+
* either the session's own JWT validation or from any React Query call
|
|
9
10
|
* via the QueryCache.onError handler).
|
|
10
11
|
*
|
|
11
|
-
* Reads `sessionExpiredAt
|
|
12
|
-
* dismisses the modal, the
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Reads `sessionExpiredAt$` from the active `FrontendSessionSignals`.
|
|
13
|
+
* When the user dismisses the modal, the signals instance clears the
|
|
14
|
+
* flag. Modal state lives on signals (not the session itself) so
|
|
15
|
+
* headless sessions (workers/CLIs) don't carry dead observables.
|
|
15
16
|
*/
|
|
16
17
|
export function SessionExpiredModal() {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} = useKnowledgeBaseSession();
|
|
18
|
+
const signals = useObservable(useSemiont().activeSignals$);
|
|
19
|
+
const sessionExpiredAt = useObservable(signals?.sessionExpiredAt$) ?? null;
|
|
20
|
+
const sessionExpiredMessage = useObservable(signals?.sessionExpiredMessage$) ?? null;
|
|
21
|
+
const acknowledgeSessionExpired = () => signals?.acknowledgeSessionExpired();
|
|
22
22
|
const showModal = sessionExpiredAt !== null;
|
|
23
23
|
|
|
24
24
|
const handleSignIn = () => {
|