@semiont/react-ui 0.5.18 → 0.5.20
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 +4 -0
- package/dist/{PdfAnnotationCanvas.client-75GY2EDR.js → PdfAnnotationCanvas.client-V7Q3BEEQ.js} +42 -45
- package/dist/PdfAnnotationCanvas.client-V7Q3BEEQ.js.map +1 -0
- package/dist/index.d.ts +231 -55
- package/dist/index.js +2115 -2161
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/components/AssistProgress.tsx +170 -0
- package/src/components/__tests__/AssistProgress.test.tsx +164 -0
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +23 -54
- package/src/components/pdf-annotation/__tests__/rects-for-page.test.ts +96 -0
- package/src/components/pdf-annotation/rects-for-page.ts +47 -0
- package/src/components/resource/ResourceViewer.tsx +23 -55
- package/src/components/resource/panels/AssessmentPanel.tsx +1 -1
- package/src/components/resource/panels/AssistSection.tsx +111 -180
- package/src/components/resource/panels/AssistShell.tsx +77 -0
- package/src/components/resource/panels/CommentsPanel.tsx +1 -1
- package/src/components/resource/panels/HighlightPanel.tsx +1 -1
- package/src/components/resource/panels/ReferenceEntry.tsx +8 -1
- package/src/components/resource/panels/ReferencesPanel.tsx +101 -132
- package/src/components/resource/panels/TaggingPanel.tsx +23 -86
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +6 -14
- package/src/components/resource/panels/__tests__/AssistShell.test.tsx +59 -0
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +20 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +14 -20
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +30 -0
- package/src/features/resource-compose/components/ResourceComposePage.tsx +19 -1
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +24 -64
- package/src/styles/features/compose.css +7 -0
- package/dist/PdfAnnotationCanvas.client-75GY2EDR.js.map +0 -1
- package/src/components/AnnotateReferencesProgressWidget.tsx +0 -125
- package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +0 -101
|
@@ -7,7 +7,7 @@ import { BrowseView, type ReferenceHover } from './BrowseView';
|
|
|
7
7
|
import { PopupContainer } from '../annotation-popups/SharedPopupElements';
|
|
8
8
|
import { JsonLdView } from '../annotation-popups/JsonLdView';
|
|
9
9
|
import type { Annotation, AnnotationId, ResourceDescriptor as SemiontResource, components, EventMap, AnchorRect } from '@semiont/core';
|
|
10
|
-
import { getExactText, getTargetSelector, isHighlight, isAssessment, isReference, isComment, isTag, getBodySource } from '@semiont/core';
|
|
10
|
+
import { getExactText, getTargetSelector, getPrimaryMediaType, isHighlight, isAssessment, isReference, isComment, isTag, getBodySource } from '@semiont/core';
|
|
11
11
|
import type { SemiontSession } from '@semiont/sdk';
|
|
12
12
|
import { useSessionEventSubscriptions } from '../../hooks/useSessionEventSubscriptions';
|
|
13
13
|
import { ANNOTATORS } from '../../lib/annotation-registry';
|
|
@@ -29,9 +29,6 @@ import type { AnnotationsCollection } from '../../types/annotation-props';
|
|
|
29
29
|
*
|
|
30
30
|
* Event flow:
|
|
31
31
|
* make-meaning → EventLog → SSE → EventBus → ResourceViewer → Cache invalidation
|
|
32
|
-
*
|
|
33
|
-
* Phase 2 complete: Event-based cache invalidation replaces manual refetch
|
|
34
|
-
* Phase 3 complete: Fully event-driven - all user interactions use unified event bus
|
|
35
32
|
*/
|
|
36
33
|
interface Props {
|
|
37
34
|
resource: SemiontResource & { content: string };
|
|
@@ -81,13 +78,14 @@ interface Props {
|
|
|
81
78
|
}
|
|
82
79
|
|
|
83
80
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
81
|
+
* Deletion calls `session.client.mark.delete(...)` directly (no bus emit);
|
|
82
|
+
* panel opening invokes the host's `onOpenPanel` callback — the host owns
|
|
83
|
+
* the panel and any `panel:open` emission.
|
|
86
84
|
*
|
|
87
85
|
* @subscribes mark:added - New annotation was added. Payload: { annotation: Annotation }
|
|
88
86
|
* @subscribes mark:removed - Annotation was removed. Payload: { annotationId: string }
|
|
89
87
|
* @subscribes mark:body-updated - Annotation was updated. Payload: { annotation: Annotation }
|
|
90
|
-
* @subscribes browse:click - User clicked on annotation. Payload: { annotationId: string }
|
|
88
|
+
* @subscribes browse:click - User clicked on annotation. Payload: { annotationId: string, motivation, anchorRect? }
|
|
91
89
|
*/
|
|
92
90
|
export function ResourceViewer({
|
|
93
91
|
resource,
|
|
@@ -125,16 +123,8 @@ export function ResourceViewer({
|
|
|
125
123
|
}
|
|
126
124
|
const rUri = resource['@id'];
|
|
127
125
|
|
|
128
|
-
//
|
|
129
|
-
const
|
|
130
|
-
const reps = resource.representations;
|
|
131
|
-
if (Array.isArray(reps) && reps.length > 0 && reps[0]) {
|
|
132
|
-
return reps[0].mediaType;
|
|
133
|
-
}
|
|
134
|
-
return 'text/plain';
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const mimeType = getMimeType();
|
|
126
|
+
// Same primary-representation semantics as the page and the worker — one helper.
|
|
127
|
+
const mimeType = getPrimaryMediaType(resource) || 'text/plain';
|
|
138
128
|
|
|
139
129
|
// Toolbar preferences (TOOLBAR-PREFS-AS-PROPS): controlled (prop supplied) or a
|
|
140
130
|
// plain uncontrolled default. Preferences are state, not events — no localStorage
|
|
@@ -152,15 +142,8 @@ export function ResourceViewer({
|
|
|
152
142
|
|
|
153
143
|
const semiont = session?.client;
|
|
154
144
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}, [semiont, rUri]);
|
|
158
|
-
|
|
159
|
-
const handleAnnotateRemoved = useCallback(() => {
|
|
160
|
-
semiont?.browse.invalidateAnnotationList(rUri);
|
|
161
|
-
}, [semiont, rUri]);
|
|
162
|
-
|
|
163
|
-
const handleAnnotateBodyUpdated = useCallback(() => {
|
|
145
|
+
// One invalidation for every annotation mutation event (added/removed/body-updated).
|
|
146
|
+
const handleAnnotationsChanged = useCallback(() => {
|
|
164
147
|
semiont?.browse.invalidateAnnotationList(rUri);
|
|
165
148
|
}, [semiont, rUri]);
|
|
166
149
|
|
|
@@ -196,20 +179,9 @@ export function ResourceViewer({
|
|
|
196
179
|
position: { x: number; y: number };
|
|
197
180
|
} | null>(null);
|
|
198
181
|
|
|
199
|
-
// Internal UI state for hover
|
|
182
|
+
// Internal UI state for hover
|
|
200
183
|
// Use prop value when provided (controlled by parent), otherwise null
|
|
201
184
|
const hoveredAnnotationId = hoveredAnnotationIdProp ?? null;
|
|
202
|
-
const [scrollToAnnotationId, setScrollToAnnotationId] = useState<string | null>(null);
|
|
203
|
-
const [_focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
|
|
204
|
-
|
|
205
|
-
// Focus annotation helper
|
|
206
|
-
const focusAnnotation = useCallback((annotationId: string) => {
|
|
207
|
-
setFocusedAnnotationId(annotationId);
|
|
208
|
-
setScrollToAnnotationId(annotationId);
|
|
209
|
-
|
|
210
|
-
// Clear focus after 3 seconds
|
|
211
|
-
setTimeout(() => setFocusedAnnotationId(null), 3000);
|
|
212
|
-
}, []);
|
|
213
185
|
|
|
214
186
|
// Calculate centered position for JSON-LD modal
|
|
215
187
|
const getJsonLdModalPosition = () => {
|
|
@@ -235,16 +207,11 @@ export function ResourceViewer({
|
|
|
235
207
|
|
|
236
208
|
// If annotation has a side panel, only open it when Detail mode is active
|
|
237
209
|
// For delete/jsonld/follow modes, let those handlers below process it
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
// Don't return early for delete/jsonld/follow modes - let them be handled below
|
|
245
|
-
if (selectedClick !== 'deleting' && selectedClick !== 'jsonld' && selectedClick !== 'follow') {
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
210
|
+
// Side-panel annotations in detail mode are routed to the host's panel by
|
|
211
|
+
// handleAnnotationClickEvent before this is ever called; here only the
|
|
212
|
+
// toolbar click modes (delete / jsonld / follow) fall through.
|
|
213
|
+
if (metadata?.hasSidePanel && selectedClick !== 'deleting' && selectedClick !== 'jsonld' && selectedClick !== 'follow') {
|
|
214
|
+
return;
|
|
248
215
|
}
|
|
249
216
|
|
|
250
217
|
// Check if this is a highlight, assessment, comment, reference, or tag
|
|
@@ -280,7 +247,7 @@ export function ResourceViewer({
|
|
|
280
247
|
setDeleteConfirmation({ annotation, position });
|
|
281
248
|
return;
|
|
282
249
|
}
|
|
283
|
-
}, [annotateMode, selectedClick,
|
|
250
|
+
}, [annotateMode, selectedClick, onOpenResource]);
|
|
284
251
|
|
|
285
252
|
// Annotation click coordinator - handles panel opening and scrolling
|
|
286
253
|
const handleAnnotationClickEvent = useCallback(({ annotationId, motivation, anchorRect }: {
|
|
@@ -317,13 +284,12 @@ export function ResourceViewer({
|
|
|
317
284
|
onOpenPanel?.({ panel: 'annotations', scrollToAnnotationId: annotationId, motivation, ...(anchorRect ? { anchorRect } : {}) });
|
|
318
285
|
}, [highlights, references, assessments, comments, tags, handleAnnotationClick, selectedClick, onOpenPanel]);
|
|
319
286
|
|
|
320
|
-
//
|
|
321
|
-
// IMPORTANT: All event subscriptions MUST be in a single call to maintain consistent hook order between renders
|
|
287
|
+
// Single subscription call per file (see scripts/compliance/audit-hooks-ordering.ts).
|
|
322
288
|
useSessionEventSubscriptions(session, {
|
|
323
289
|
// Annotation cache invalidation
|
|
324
|
-
'mark:added':
|
|
325
|
-
'mark:removed':
|
|
326
|
-
'mark:body-updated':
|
|
290
|
+
'mark:added': handleAnnotationsChanged,
|
|
291
|
+
'mark:removed': handleAnnotationsChanged,
|
|
292
|
+
'mark:body-updated': handleAnnotationsChanged,
|
|
327
293
|
|
|
328
294
|
// Annotation clicks
|
|
329
295
|
'browse:click': handleAnnotationClickEvent,
|
|
@@ -335,12 +301,14 @@ export function ResourceViewer({
|
|
|
335
301
|
[highlights, references, assessments, comments, tags]
|
|
336
302
|
);
|
|
337
303
|
|
|
304
|
+
// No scrollToAnnotationId: panel-entry scrolling is the host's loop (page →
|
|
305
|
+
// UnifiedAnnotationsPanel → onScrollCompleted); in-content scrolling stays a
|
|
306
|
+
// host-facing capability on AnnotateView/CodeMirrorRenderer, unfed here.
|
|
338
307
|
const uiState = {
|
|
339
308
|
selectedMotivation,
|
|
340
309
|
selectedClick,
|
|
341
310
|
selectedShape,
|
|
342
311
|
hoveredAnnotationId,
|
|
343
|
-
scrollToAnnotationId
|
|
344
312
|
};
|
|
345
313
|
|
|
346
314
|
// Define getTargetResourceName callback OUTSIDE the conditional
|
|
@@ -245,7 +245,7 @@ export function AssessmentPanel({
|
|
|
245
245
|
|
|
246
246
|
{/* Scrollable content area */}
|
|
247
247
|
<div ref={containerRef} className="semiont-panel__content">
|
|
248
|
-
{/* Assist Section - only in Annotate mode
|
|
248
|
+
{/* Assist Section - only in Annotate mode; shown for any media type (AI detection is media-agnostic — text is resolved via the media-type registry, incl. PDF text layers) */}
|
|
249
249
|
{annotateMode && (
|
|
250
250
|
<AssistSection
|
|
251
251
|
session={session}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState,
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
5
|
import type { SemiontSession } from '@semiont/sdk';
|
|
6
6
|
import type { Motivation, components } from '@semiont/core';
|
|
7
|
+
import { AssistShell } from './AssistShell';
|
|
7
8
|
import './AssistSection.css';
|
|
8
9
|
|
|
9
10
|
type JobProgress = components['schemas']['JobProgress'];
|
|
@@ -20,16 +21,11 @@ interface AssistSectionProps {
|
|
|
20
21
|
progress?: JobProgress | null | undefined;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
// Color schemes are now handled via CSS data attributes
|
|
24
|
-
|
|
25
24
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* - Optional tone selector (for comments)
|
|
31
|
-
* - Assist button with sparkle animation
|
|
32
|
-
* - Progress display during annotation assist
|
|
25
|
+
* Assist fields for the text motivations (highlight, assessment, comment):
|
|
26
|
+
* instructions, tone (comment/assessment), density — composed into the shared
|
|
27
|
+
* AssistShell chrome. Reference and tag panels compose the same shell with
|
|
28
|
+
* their own fields (entity chips; schema + categories).
|
|
33
29
|
*
|
|
34
30
|
* @emits mark:assist-request - Start assist for annotation type. Payload: { motivation: Motivation, options: { instructions?: string, tone?: string, density?: number } }
|
|
35
31
|
* @emits mark:progress-dismiss - Dismiss the annotation progress display
|
|
@@ -51,23 +47,10 @@ export function AssistSection({
|
|
|
51
47
|
type ToneValue = 'scholarly' | 'explanatory' | 'conversational' | 'technical' | 'analytical' | 'critical' | 'balanced' | 'constructive' | '';
|
|
52
48
|
const [tone, setTone] = useState<ToneValue>('');
|
|
53
49
|
// Default density depends on annotation type
|
|
54
|
-
const defaultDensity = annotationType === '
|
|
50
|
+
const defaultDensity = annotationType === 'assessment' ? 4 : 5;
|
|
55
51
|
const [density, setDensity] = useState(defaultDensity);
|
|
56
52
|
const [useDensity, setUseDensity] = useState(true); // Enabled by default
|
|
57
53
|
|
|
58
|
-
// Collapsible section state - load from localStorage, default expanded
|
|
59
|
-
const [isExpanded, setIsExpanded] = useState(() => {
|
|
60
|
-
if (typeof window === 'undefined') return true;
|
|
61
|
-
const stored = localStorage.getItem(`assist-section-expanded-${annotationType}`);
|
|
62
|
-
return stored ? stored === 'true' : true;
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// Persist expanded state to localStorage
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
if (typeof window === 'undefined') return;
|
|
68
|
-
localStorage.setItem(`assist-section-expanded-${annotationType}`, String(isExpanded));
|
|
69
|
-
}, [isExpanded, annotationType]);
|
|
70
|
-
|
|
71
54
|
const handleAssist = useCallback(() => {
|
|
72
55
|
// Map annotation type to motivation
|
|
73
56
|
const motivation: Motivation =
|
|
@@ -78,7 +61,7 @@ export function AssistSection({
|
|
|
78
61
|
session?.client.mark.requestAssist(motivation, {
|
|
79
62
|
instructions: instructions.trim() || undefined,
|
|
80
63
|
tone: (annotationType === 'comment' || annotationType === 'assessment') && tone ? tone : undefined,
|
|
81
|
-
density:
|
|
64
|
+
density: useDensity ? density : undefined,
|
|
82
65
|
// Body locale only applies where the LLM writes natural-language text:
|
|
83
66
|
// comment/assessment have a body, highlight does not.
|
|
84
67
|
language: (annotationType === 'comment' || annotationType === 'assessment') ? locale : undefined,
|
|
@@ -97,169 +80,117 @@ export function AssistSection({
|
|
|
97
80
|
}, [session]);
|
|
98
81
|
|
|
99
82
|
return (
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
83
|
+
<AssistShell
|
|
84
|
+
assistType={annotationType}
|
|
85
|
+
title={t(annotationType === 'highlight' ? 'annotateHighlights' :
|
|
86
|
+
annotationType === 'assessment' ? 'annotateAssessments' :
|
|
87
|
+
'annotateComments')}
|
|
88
|
+
isAssisting={isAssisting}
|
|
89
|
+
progress={progress}
|
|
90
|
+
progressProps={{
|
|
91
|
+
onDismiss: handleDismissProgress,
|
|
92
|
+
translations: { close: t('closeProgress') },
|
|
93
|
+
}}
|
|
94
|
+
form={
|
|
95
|
+
<>
|
|
96
|
+
<div className="semiont-form-field">
|
|
97
|
+
<label className="semiont-form-field__label">
|
|
98
|
+
{t('instructions')} {t('optional')}
|
|
99
|
+
</label>
|
|
100
|
+
<textarea
|
|
101
|
+
value={instructions}
|
|
102
|
+
onChange={(e) => setInstructions(e.target.value)}
|
|
103
|
+
className="semiont-textarea"
|
|
104
|
+
rows={3}
|
|
105
|
+
placeholder={t('instructionsPlaceholder')}
|
|
106
|
+
maxLength={500}
|
|
107
|
+
/>
|
|
108
|
+
<div className="semiont-form-field__char-count">
|
|
109
|
+
{instructions.length}/500
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{/* Tone selector - for comments and assessments */}
|
|
114
|
+
{(annotationType === 'comment' || annotationType === 'assessment') && (
|
|
125
115
|
<div className="semiont-form-field">
|
|
126
116
|
<label className="semiont-form-field__label">
|
|
127
|
-
{t('
|
|
117
|
+
{t('toneLabel')} {t('toneOptional')}
|
|
128
118
|
</label>
|
|
129
|
-
<
|
|
130
|
-
value={
|
|
131
|
-
onChange={(e) =>
|
|
132
|
-
className="semiont-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
/>
|
|
137
|
-
<div className="semiont-form-field__char-count">
|
|
138
|
-
{instructions.length}/500
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
|
-
|
|
142
|
-
{/* Tone selector - for comments and assessments */}
|
|
143
|
-
{(annotationType === 'comment' || annotationType === 'assessment') && (
|
|
144
|
-
<div className="semiont-form-field">
|
|
145
|
-
<label className="semiont-form-field__label">
|
|
146
|
-
{t('toneLabel')} {t('toneOptional')}
|
|
147
|
-
</label>
|
|
148
|
-
<select
|
|
149
|
-
value={tone}
|
|
150
|
-
onChange={(e) => setTone(e.target.value as ToneValue)}
|
|
151
|
-
className="semiont-select"
|
|
152
|
-
>
|
|
153
|
-
<option value="">Default</option>
|
|
154
|
-
{annotationType === 'comment' && (
|
|
155
|
-
<>
|
|
156
|
-
<option value="scholarly">{t('toneScholarly')}</option>
|
|
157
|
-
<option value="explanatory">{t('toneExplanatory')}</option>
|
|
158
|
-
<option value="conversational">{t('toneConversational')}</option>
|
|
159
|
-
<option value="technical">{t('toneTechnical')}</option>
|
|
160
|
-
</>
|
|
161
|
-
)}
|
|
162
|
-
{annotationType === 'assessment' && (
|
|
163
|
-
<>
|
|
164
|
-
<option value="analytical">{t('toneAnalytical')}</option>
|
|
165
|
-
<option value="critical">{t('toneCritical')}</option>
|
|
166
|
-
<option value="balanced">{t('toneBalanced')}</option>
|
|
167
|
-
<option value="constructive">{t('toneConstructive')}</option>
|
|
168
|
-
</>
|
|
169
|
-
)}
|
|
170
|
-
</select>
|
|
171
|
-
</div>
|
|
172
|
-
)}
|
|
173
|
-
|
|
174
|
-
{/* Density selector - for comments, assessments, and highlights */}
|
|
175
|
-
{(annotationType === 'comment' || annotationType === 'assessment' || annotationType === 'highlight') && (
|
|
176
|
-
<div className="semiont-form-field">
|
|
177
|
-
{/* Header with toggle */}
|
|
178
|
-
<div className="semiont-form-field__header">
|
|
179
|
-
<label className="semiont-form-field__label semiont-form-field__label--with-checkbox">
|
|
180
|
-
<input
|
|
181
|
-
type="checkbox"
|
|
182
|
-
checked={useDensity}
|
|
183
|
-
onChange={(e) => setUseDensity(e.target.checked)}
|
|
184
|
-
className="semiont-checkbox"
|
|
185
|
-
data-variant={annotationType}
|
|
186
|
-
/>
|
|
187
|
-
<span>{t('densityLabel')}</span>
|
|
188
|
-
</label>
|
|
189
|
-
{useDensity && (
|
|
190
|
-
<span className="semiont-form-field__info">{density} per 2000 words</span>
|
|
191
|
-
)}
|
|
192
|
-
</div>
|
|
193
|
-
|
|
194
|
-
{/* Slider - only shown when enabled */}
|
|
195
|
-
{useDensity && (
|
|
119
|
+
<select
|
|
120
|
+
value={tone}
|
|
121
|
+
onChange={(e) => setTone(e.target.value as ToneValue)}
|
|
122
|
+
className="semiont-select"
|
|
123
|
+
>
|
|
124
|
+
<option value="">Default</option>
|
|
125
|
+
{annotationType === 'comment' && (
|
|
196
126
|
<>
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
value={density}
|
|
202
|
-
onChange={(e) => setDensity(Number(e.target.value))}
|
|
203
|
-
className="semiont-slider"
|
|
204
|
-
/>
|
|
205
|
-
<div className="semiont-slider__labels">
|
|
206
|
-
<span>{t('densitySparse')}</span>
|
|
207
|
-
<span>{t('densityDense')}</span>
|
|
208
|
-
</div>
|
|
127
|
+
<option value="scholarly">{t('toneScholarly')}</option>
|
|
128
|
+
<option value="explanatory">{t('toneExplanatory')}</option>
|
|
129
|
+
<option value="conversational">{t('toneConversational')}</option>
|
|
130
|
+
<option value="technical">{t('toneTechnical')}</option>
|
|
209
131
|
</>
|
|
210
132
|
)}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
)}
|
|
240
|
-
|
|
241
|
-
<div className="semiont-annotation-progress__status">
|
|
242
|
-
<div className="semiont-annotation-progress__message">
|
|
243
|
-
<span className="semiont-annotation-progress__icon">✨</span>
|
|
244
|
-
<span>{progress.message}</span>
|
|
245
|
-
</div>
|
|
246
|
-
{/* Close button - shown after assist completes (when not actively assisting) */}
|
|
247
|
-
{!isAssisting && (
|
|
248
|
-
<button
|
|
249
|
-
onClick={handleDismissProgress}
|
|
250
|
-
className="semiont-annotation-progress__close"
|
|
251
|
-
aria-label={t('closeProgress')}
|
|
252
|
-
title={t('closeProgress')}
|
|
253
|
-
type="button"
|
|
254
|
-
>
|
|
255
|
-
×
|
|
256
|
-
</button>
|
|
133
|
+
{annotationType === 'assessment' && (
|
|
134
|
+
<>
|
|
135
|
+
<option value="analytical">{t('toneAnalytical')}</option>
|
|
136
|
+
<option value="critical">{t('toneCritical')}</option>
|
|
137
|
+
<option value="balanced">{t('toneBalanced')}</option>
|
|
138
|
+
<option value="constructive">{t('toneConstructive')}</option>
|
|
139
|
+
</>
|
|
140
|
+
)}
|
|
141
|
+
</select>
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
{/* Density selector — applies to every assist type */}
|
|
146
|
+
<div className="semiont-form-field">
|
|
147
|
+
{/* Header with toggle */}
|
|
148
|
+
<div className="semiont-form-field__header">
|
|
149
|
+
<label className="semiont-form-field__label semiont-form-field__label--with-checkbox">
|
|
150
|
+
<input
|
|
151
|
+
type="checkbox"
|
|
152
|
+
checked={useDensity}
|
|
153
|
+
onChange={(e) => setUseDensity(e.target.checked)}
|
|
154
|
+
className="semiont-checkbox"
|
|
155
|
+
data-variant={annotationType}
|
|
156
|
+
/>
|
|
157
|
+
<span>{t('densityLabel')}</span>
|
|
158
|
+
</label>
|
|
159
|
+
{useDensity && (
|
|
160
|
+
<span className="semiont-form-field__info">{density} per 2000 words</span>
|
|
257
161
|
)}
|
|
258
162
|
</div>
|
|
163
|
+
|
|
164
|
+
{/* Slider - only shown when enabled */}
|
|
165
|
+
{useDensity && (
|
|
166
|
+
<>
|
|
167
|
+
<input
|
|
168
|
+
type="range"
|
|
169
|
+
min={annotationType === 'comment' ? '2' : '1'}
|
|
170
|
+
max={annotationType === 'comment' ? '12' : annotationType === 'assessment' ? '10' : '15'}
|
|
171
|
+
value={density}
|
|
172
|
+
onChange={(e) => setDensity(Number(e.target.value))}
|
|
173
|
+
className="semiont-slider"
|
|
174
|
+
/>
|
|
175
|
+
<div className="semiont-slider__labels">
|
|
176
|
+
<span>{t('densitySparse')}</span>
|
|
177
|
+
<span>{t('densityDense')}</span>
|
|
178
|
+
</div>
|
|
179
|
+
</>
|
|
180
|
+
)}
|
|
259
181
|
</div>
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
182
|
+
|
|
183
|
+
<button
|
|
184
|
+
onClick={handleAssist}
|
|
185
|
+
className="semiont-button"
|
|
186
|
+
data-variant="assist"
|
|
187
|
+
data-type={annotationType}
|
|
188
|
+
>
|
|
189
|
+
<span className="semiont-button-icon">✨</span>
|
|
190
|
+
<span>{t('annotate')}</span>
|
|
191
|
+
</button>
|
|
192
|
+
</>
|
|
193
|
+
}
|
|
194
|
+
/>
|
|
264
195
|
);
|
|
265
196
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, type ReactNode } from 'react';
|
|
4
|
+
import type { components } from '@semiont/core';
|
|
5
|
+
import { AssistProgress, type AssistProgressProps } from '../../AssistProgress';
|
|
6
|
+
|
|
7
|
+
type JobProgress = components['schemas']['JobProgress'];
|
|
8
|
+
|
|
9
|
+
export interface AssistShellProps {
|
|
10
|
+
/** localStorage persist-key suffix and CSS `data-type` ('highlight' | 'comment' | 'assessment' | 'reference' | 'tag'). */
|
|
11
|
+
assistType: string;
|
|
12
|
+
/** Collapsible section title (already translated). */
|
|
13
|
+
title: string;
|
|
14
|
+
isAssisting: boolean;
|
|
15
|
+
progress: JobProgress | null | undefined;
|
|
16
|
+
/** The per-motivation form (fields + submit) — shown when no progress is displayed. */
|
|
17
|
+
form: ReactNode;
|
|
18
|
+
/**
|
|
19
|
+
* Pass-through config for the progress renderer (cancel/dismiss wiring,
|
|
20
|
+
* translations, percent bar). Dismiss policy lives HERE: the shell forwards
|
|
21
|
+
* `onDismiss` only once the assist is no longer running.
|
|
22
|
+
*/
|
|
23
|
+
progressProps?: Omit<AssistProgressProps, 'progress' | 'dataType'>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The one assist-section chrome (#7): collapsible header with persisted expand
|
|
28
|
+
* state, the assisting wrapper, and the form-vs-progress switch. Every
|
|
29
|
+
* motivation's panel composes this shell with its own fields — the fields
|
|
30
|
+
* differ per motivation by design (instructions/tone/density vs entity chips
|
|
31
|
+
* vs schema+categories), so the shell owns only what is genuinely shared.
|
|
32
|
+
*/
|
|
33
|
+
export function AssistShell({ assistType, title, isAssisting, progress, form, progressProps }: AssistShellProps) {
|
|
34
|
+
const [isExpanded, setIsExpanded] = useState(() => {
|
|
35
|
+
if (typeof window === 'undefined') return true;
|
|
36
|
+
const stored = localStorage.getItem(`assist-section-expanded-${assistType}`);
|
|
37
|
+
return stored ? stored === 'true' : true;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (typeof window === 'undefined') return;
|
|
42
|
+
localStorage.setItem(`assist-section-expanded-${assistType}`, String(isExpanded));
|
|
43
|
+
}, [isExpanded, assistType]);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="semiont-panel__section">
|
|
47
|
+
<button
|
|
48
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
49
|
+
className="semiont-panel__section-title semiont-panel__section-title--collapsible"
|
|
50
|
+
aria-expanded={isExpanded}
|
|
51
|
+
type="button"
|
|
52
|
+
>
|
|
53
|
+
<span>{title}</span>
|
|
54
|
+
<span className="semiont-panel__section-chevron" data-expanded={isExpanded}>
|
|
55
|
+
›
|
|
56
|
+
</span>
|
|
57
|
+
</button>
|
|
58
|
+
{isExpanded && (
|
|
59
|
+
<div
|
|
60
|
+
className="semiont-assist-widget"
|
|
61
|
+
data-assisting={isAssisting && progress ? 'true' : 'false'}
|
|
62
|
+
data-type={assistType}
|
|
63
|
+
>
|
|
64
|
+
{!progress && form}
|
|
65
|
+
{progress && (
|
|
66
|
+
<AssistProgress
|
|
67
|
+
progress={progress}
|
|
68
|
+
dataType={assistType}
|
|
69
|
+
{...progressProps}
|
|
70
|
+
{...(isAssisting ? { onDismiss: undefined } : {})}
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -255,7 +255,7 @@ export function CommentsPanel({
|
|
|
255
255
|
|
|
256
256
|
{/* Scrollable content area */}
|
|
257
257
|
<div ref={containerRef} className="semiont-panel__content">
|
|
258
|
-
{/* Assist Section - only in Annotate mode
|
|
258
|
+
{/* Assist Section - only in Annotate mode; shown for any media type (AI detection is media-agnostic — text is resolved via the media-type registry, incl. PDF text layers) */}
|
|
259
259
|
{annotateMode && (
|
|
260
260
|
<AssistSection
|
|
261
261
|
session={session}
|
|
@@ -153,7 +153,7 @@ export function HighlightPanel({
|
|
|
153
153
|
|
|
154
154
|
{/* Scrollable content area */}
|
|
155
155
|
<div ref={containerRef} className="semiont-panel__content">
|
|
156
|
-
{/* Assist Section - only in Annotate mode
|
|
156
|
+
{/* Assist Section - only in Annotate mode; shown for any media type (AI detection is media-agnostic — text is resolved via the media-type registry, incl. PDF text layers) */}
|
|
157
157
|
{annotateMode && (
|
|
158
158
|
<AssistSection
|
|
159
159
|
session={session}
|
|
@@ -78,7 +78,14 @@ export function ReferenceEntry({
|
|
|
78
78
|
resourceId(source),
|
|
79
79
|
reference.id,
|
|
80
80
|
[{ op: 'remove', item: { type: 'SpecificResource', source: resolvedResourceUri, purpose: 'linking' } }],
|
|
81
|
-
).catch(() => {
|
|
81
|
+
).catch((error: unknown) => {
|
|
82
|
+
// This component has no toast surface — report the client-local,
|
|
83
|
+
// resource-stamped bind error; useOutcomeToasts surfaces it.
|
|
84
|
+
semiont.bind.reportBodyError({
|
|
85
|
+
resourceId: source,
|
|
86
|
+
message: error instanceof Error ? error.message : String(error),
|
|
87
|
+
});
|
|
88
|
+
});
|
|
82
89
|
}
|
|
83
90
|
};
|
|
84
91
|
|