@semiont/react-ui 0.2.33-build.77 → 0.2.33-build.79
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/dist/{ar-RNNSPLQB.mjs → ar-EMHEHPCJ.mjs} +2 -1
- package/dist/ar-EMHEHPCJ.mjs.map +1 -0
- package/dist/{bn-S2CDL7EC.mjs → bn-OVCI4F6X.mjs} +2 -1
- package/dist/bn-OVCI4F6X.mjs.map +1 -0
- package/dist/{chunk-35LLVRFK.mjs → chunk-JZIO2A3B.mjs} +31 -31
- package/dist/{chunk-UDX2Q35T.mjs → chunk-LIHZTECW.mjs} +2 -1
- package/dist/chunk-LIHZTECW.mjs.map +1 -0
- package/dist/{cs-RSV675WU.mjs → cs-FAN66Q2F.mjs} +2 -1
- package/dist/cs-FAN66Q2F.mjs.map +1 -0
- package/dist/{da-CHXNPWJC.mjs → da-YBBIHI2O.mjs} +2 -1
- package/dist/da-YBBIHI2O.mjs.map +1 -0
- package/dist/{de-KPEZ53D4.mjs → de-MAYU33LB.mjs} +2 -1
- package/dist/de-MAYU33LB.mjs.map +1 -0
- package/dist/{el-MW2BME5T.mjs → el-MKGSWN4O.mjs} +2 -1
- package/dist/el-MKGSWN4O.mjs.map +1 -0
- package/dist/{en-EVMIX24Y.mjs → en-DDLIXJCU.mjs} +2 -2
- package/dist/{es-HQ24NYS3.mjs → es-52LHUWJD.mjs} +2 -1
- package/dist/es-52LHUWJD.mjs.map +1 -0
- package/dist/{fa-W34LRLHG.mjs → fa-FJICRANB.mjs} +2 -1
- package/dist/fa-FJICRANB.mjs.map +1 -0
- package/dist/{fi-3U44IGOA.mjs → fi-O455XFCR.mjs} +2 -1
- package/dist/fi-O455XFCR.mjs.map +1 -0
- package/dist/{fr-N7DKX6NN.mjs → fr-TXIXHOOE.mjs} +2 -1
- package/dist/fr-TXIXHOOE.mjs.map +1 -0
- package/dist/{he-CS4WRXN3.mjs → he-JBSOX5IN.mjs} +2 -1
- package/dist/he-JBSOX5IN.mjs.map +1 -0
- package/dist/{hi-GJDY46KA.mjs → hi-KGHI3XVT.mjs} +2 -1
- package/dist/hi-KGHI3XVT.mjs.map +1 -0
- package/dist/{id-WAEZJK2Y.mjs → id-5OCPPZLO.mjs} +2 -1
- package/dist/id-5OCPPZLO.mjs.map +1 -0
- package/dist/index.d.mts +102 -106
- package/dist/index.mjs +1814 -1450
- package/dist/index.mjs.map +1 -1
- package/dist/{it-VDNDMZPU.mjs → it-PNBBZSM2.mjs} +2 -1
- package/dist/it-PNBBZSM2.mjs.map +1 -0
- package/dist/{ja-5PEH56J5.mjs → ja-LDD7R3TJ.mjs} +2 -1
- package/dist/ja-LDD7R3TJ.mjs.map +1 -0
- package/dist/{ko-JYPL3WVA.mjs → ko-F47ZDEY3.mjs} +2 -1
- package/dist/ko-F47ZDEY3.mjs.map +1 -0
- package/dist/{ms-5PZVW76T.mjs → ms-Z7LMXJWL.mjs} +2 -1
- package/dist/ms-Z7LMXJWL.mjs.map +1 -0
- package/dist/{nl-YXES36KM.mjs → nl-6SJFBPJ3.mjs} +2 -1
- package/dist/nl-6SJFBPJ3.mjs.map +1 -0
- package/dist/{no-XRA2UCQD.mjs → no-YXPBPSGF.mjs} +2 -1
- package/dist/no-YXPBPSGF.mjs.map +1 -0
- package/dist/{pl-WH6LJA5G.mjs → pl-P4AZ2QME.mjs} +2 -1
- package/dist/pl-P4AZ2QME.mjs.map +1 -0
- package/dist/{pt-7GAG57BM.mjs → pt-LHWUS6U6.mjs} +2 -1
- package/dist/pt-LHWUS6U6.mjs.map +1 -0
- package/dist/{ro-BTDDRB7N.mjs → ro-EA5J2ZON.mjs} +2 -1
- package/dist/ro-EA5J2ZON.mjs.map +1 -0
- package/dist/{sv-7V5C2IT4.mjs → sv-DATBS3UQ.mjs} +2 -1
- package/dist/sv-DATBS3UQ.mjs.map +1 -0
- package/dist/test-utils.mjs +2 -2
- package/dist/{th-LPKYLBX5.mjs → th-WTFJRWPT.mjs} +2 -1
- package/dist/th-WTFJRWPT.mjs.map +1 -0
- package/dist/{tr-DU4RQL4M.mjs → tr-IKO3RXOX.mjs} +2 -1
- package/dist/tr-IKO3RXOX.mjs.map +1 -0
- package/dist/{uk-36UHTDDI.mjs → uk-CF6CTTRK.mjs} +2 -1
- package/dist/uk-CF6CTTRK.mjs.map +1 -0
- package/dist/{vi-GDHOUZDH.mjs → vi-AJLTXPZQ.mjs} +2 -1
- package/dist/vi-AJLTXPZQ.mjs.map +1 -0
- package/dist/{zh-TYUID4XZ.mjs → zh-U3ORHHYH.mjs} +2 -1
- package/dist/zh-U3ORHHYH.mjs.map +1 -0
- package/package.json +6 -2
- package/src/components/resource/AnnotateView.tsx +0 -4
- package/src/components/resource/AnnotationHistory.tsx +12 -13
- package/src/components/resource/BrowseView.tsx +8 -16
- package/src/components/resource/HistoryEvent.tsx +3 -4
- package/src/components/resource/ResourceViewer.tsx +174 -201
- package/src/components/resource/event-formatting.ts +316 -0
- package/src/components/resource/panels/AssessmentPanel.tsx +37 -9
- package/src/components/resource/panels/CollaborationPanel.tsx +20 -13
- package/src/components/resource/panels/CommentsPanel.tsx +38 -9
- package/src/components/resource/panels/ReferencesPanel.tsx +39 -14
- package/src/components/resource/panels/StatisticsPanel.tsx +9 -19
- package/src/components/resource/panels/TaggingPanel.tsx +27 -0
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +28 -21
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +547 -0
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +10 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +10 -0
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +564 -0
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +8 -15
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +13 -6
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +147 -78
- package/src/styles/motivations/motivation-assessment.css +28 -0
- package/src/styles/patterns/panel-helpers.css +26 -0
- package/translations/ar.json +1 -0
- package/translations/bn.json +1 -0
- package/translations/cs.json +1 -0
- package/translations/da.json +1 -0
- package/translations/de.json +1 -0
- package/translations/el.json +1 -0
- package/translations/en.json +1 -0
- package/translations/es.json +1 -0
- package/translations/fa.json +1 -0
- package/translations/fi.json +1 -0
- package/translations/fr.json +1 -0
- package/translations/he.json +1 -0
- package/translations/hi.json +1 -0
- package/translations/id.json +1 -0
- package/translations/it.json +1 -0
- package/translations/ja.json +1 -0
- package/translations/ko.json +1 -0
- package/translations/ms.json +1 -0
- package/translations/nl.json +1 -0
- package/translations/no.json +1 -0
- package/translations/pl.json +1 -0
- package/translations/pt.json +1 -0
- package/translations/ro.json +1 -0
- package/translations/sv.json +1 -0
- package/translations/th.json +1 -0
- package/translations/tr.json +1 -0
- package/translations/uk.json +1 -0
- package/translations/vi.json +1 -0
- package/translations/zh.json +1 -0
- package/dist/ar-RNNSPLQB.mjs.map +0 -1
- package/dist/bn-S2CDL7EC.mjs.map +0 -1
- package/dist/chunk-UDX2Q35T.mjs.map +0 -1
- package/dist/cs-RSV675WU.mjs.map +0 -1
- package/dist/da-CHXNPWJC.mjs.map +0 -1
- package/dist/de-KPEZ53D4.mjs.map +0 -1
- package/dist/el-MW2BME5T.mjs.map +0 -1
- package/dist/es-HQ24NYS3.mjs.map +0 -1
- package/dist/fa-W34LRLHG.mjs.map +0 -1
- package/dist/fi-3U44IGOA.mjs.map +0 -1
- package/dist/fr-N7DKX6NN.mjs.map +0 -1
- package/dist/he-CS4WRXN3.mjs.map +0 -1
- package/dist/hi-GJDY46KA.mjs.map +0 -1
- package/dist/id-WAEZJK2Y.mjs.map +0 -1
- package/dist/it-VDNDMZPU.mjs.map +0 -1
- package/dist/ja-5PEH56J5.mjs.map +0 -1
- package/dist/ko-JYPL3WVA.mjs.map +0 -1
- package/dist/ms-5PZVW76T.mjs.map +0 -1
- package/dist/nl-YXES36KM.mjs.map +0 -1
- package/dist/no-XRA2UCQD.mjs.map +0 -1
- package/dist/pl-WH6LJA5G.mjs.map +0 -1
- package/dist/pt-7GAG57BM.mjs.map +0 -1
- package/dist/ro-BTDDRB7N.mjs.map +0 -1
- package/dist/sv-7V5C2IT4.mjs.map +0 -1
- package/dist/th-LPKYLBX5.mjs.map +0 -1
- package/dist/tr-DU4RQL4M.mjs.map +0 -1
- package/dist/uk-36UHTDDI.mjs.map +0 -1
- package/dist/vi-GDHOUZDH.mjs.map +0 -1
- package/dist/zh-TYUID4XZ.mjs.map +0 -1
- /package/dist/{chunk-35LLVRFK.mjs.map → chunk-JZIO2A3B.mjs.map} +0 -0
- /package/dist/{en-EVMIX24Y.mjs.map → en-DDLIXJCU.mjs.map} +0 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Formatting Utilities
|
|
3
|
+
*
|
|
4
|
+
* Display and formatting utilities for resource events.
|
|
5
|
+
* No React dependencies - safe to use in any JavaScript environment.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { StoredEvent, ResourceEventType } from '@semiont/core';
|
|
9
|
+
import type { components } from '@semiont/api-client';
|
|
10
|
+
import { getExactText, getTargetSelector } from '@semiont/api-client';
|
|
11
|
+
import { ANNOTATORS } from '../../lib/annotation-registry';
|
|
12
|
+
|
|
13
|
+
type Annotation = components['schemas']['Annotation'];
|
|
14
|
+
type TranslateFn = (key: string, params?: Record<string, string | number>) => string;
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// EVENT FORMATTING AND DISPLAY
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Format event type for display with i18n support
|
|
22
|
+
*/
|
|
23
|
+
export function formatEventType(type: ResourceEventType, t: TranslateFn, payload?: any): string {
|
|
24
|
+
switch (type) {
|
|
25
|
+
case 'resource.created':
|
|
26
|
+
return t('resourceCreated');
|
|
27
|
+
case 'resource.cloned':
|
|
28
|
+
return t('resourceCloned');
|
|
29
|
+
case 'resource.archived':
|
|
30
|
+
return t('resourceArchived');
|
|
31
|
+
case 'resource.unarchived':
|
|
32
|
+
return t('resourceUnarchived');
|
|
33
|
+
|
|
34
|
+
case 'annotation.added': {
|
|
35
|
+
const motivation = payload?.annotation?.motivation;
|
|
36
|
+
if (motivation === 'highlighting') return t('highlightAdded');
|
|
37
|
+
if (motivation === 'linking') return t('referenceCreated');
|
|
38
|
+
if (motivation === 'assessing') return t('assessmentAdded');
|
|
39
|
+
return t('annotationAdded');
|
|
40
|
+
}
|
|
41
|
+
case 'annotation.removed': {
|
|
42
|
+
return t('annotationRemoved');
|
|
43
|
+
}
|
|
44
|
+
case 'annotation.body.updated': {
|
|
45
|
+
return t('annotationBodyUpdated');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
case 'entitytag.added':
|
|
49
|
+
return t('entitytagAdded');
|
|
50
|
+
case 'entitytag.removed':
|
|
51
|
+
return t('entitytagRemoved');
|
|
52
|
+
|
|
53
|
+
case 'job.completed':
|
|
54
|
+
case 'job.started':
|
|
55
|
+
case 'job.progress':
|
|
56
|
+
case 'job.failed':
|
|
57
|
+
return t('jobEvent');
|
|
58
|
+
|
|
59
|
+
case 'representation.added':
|
|
60
|
+
case 'representation.removed':
|
|
61
|
+
return t('representationEvent');
|
|
62
|
+
|
|
63
|
+
default:
|
|
64
|
+
return type;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get emoji for event type
|
|
70
|
+
* For unified annotation events, pass the payload to determine motivation
|
|
71
|
+
*/
|
|
72
|
+
export function getEventEmoji(type: ResourceEventType, payload?: any): string {
|
|
73
|
+
switch (type) {
|
|
74
|
+
case 'resource.created':
|
|
75
|
+
case 'resource.cloned':
|
|
76
|
+
case 'resource.archived':
|
|
77
|
+
case 'resource.unarchived':
|
|
78
|
+
return '📄';
|
|
79
|
+
|
|
80
|
+
case 'annotation.added': {
|
|
81
|
+
const motivation = payload?.annotation?.motivation;
|
|
82
|
+
// Use annotation registry as single source of truth for emojis
|
|
83
|
+
if (motivation === 'highlighting') return ANNOTATORS.highlight.iconEmoji || '📝';
|
|
84
|
+
if (motivation === 'linking') return ANNOTATORS.reference.iconEmoji || '📝';
|
|
85
|
+
if (motivation === 'assessing') return ANNOTATORS.assessment.iconEmoji || '📝';
|
|
86
|
+
return '📝';
|
|
87
|
+
}
|
|
88
|
+
case 'annotation.removed': {
|
|
89
|
+
return '🗑️';
|
|
90
|
+
}
|
|
91
|
+
case 'annotation.body.updated': {
|
|
92
|
+
return '✏️';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case 'entitytag.added':
|
|
96
|
+
case 'entitytag.removed':
|
|
97
|
+
return '🏷️';
|
|
98
|
+
|
|
99
|
+
case 'job.completed':
|
|
100
|
+
return '🔗'; // Link emoji for linked document creation
|
|
101
|
+
case 'job.started':
|
|
102
|
+
case 'job.progress':
|
|
103
|
+
return '⚙️'; // Gear for job processing
|
|
104
|
+
case 'job.failed':
|
|
105
|
+
return '❌'; // X mark for failed jobs
|
|
106
|
+
|
|
107
|
+
case 'representation.added':
|
|
108
|
+
case 'representation.removed':
|
|
109
|
+
return '📄';
|
|
110
|
+
|
|
111
|
+
default:
|
|
112
|
+
return '📝';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Format timestamp as relative time with i18n support
|
|
118
|
+
*/
|
|
119
|
+
export function formatRelativeTime(timestamp: string, t: TranslateFn): string {
|
|
120
|
+
const date = new Date(timestamp);
|
|
121
|
+
const now = new Date();
|
|
122
|
+
const diffMs = now.getTime() - date.getTime();
|
|
123
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
124
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
125
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
126
|
+
|
|
127
|
+
if (diffMins < 1) return t('justNow');
|
|
128
|
+
if (diffMins < 60) return t('minutesAgo', { count: diffMins });
|
|
129
|
+
if (diffHours < 24) return t('hoursAgo', { count: diffHours });
|
|
130
|
+
if (diffDays < 7) return t('daysAgo', { count: diffDays });
|
|
131
|
+
|
|
132
|
+
return date.toLocaleDateString();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Helper to truncate text for display
|
|
137
|
+
*/
|
|
138
|
+
function truncateText(text: string, maxLength = 50): string {
|
|
139
|
+
const trimmed = text.trim();
|
|
140
|
+
return trimmed.length > maxLength ? trimmed.substring(0, maxLength) + '...' : trimmed;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get display content from event payload - complete implementation
|
|
145
|
+
*/
|
|
146
|
+
export function getEventDisplayContent(
|
|
147
|
+
event: StoredEvent,
|
|
148
|
+
annotations: Annotation[], // Unified annotations array (all types)
|
|
149
|
+
allEvents: StoredEvent[]
|
|
150
|
+
): { exact: string; isQuoted: boolean; isTag: boolean } | null {
|
|
151
|
+
const eventData = event.event;
|
|
152
|
+
|
|
153
|
+
// Use type discriminators for proper narrowing
|
|
154
|
+
switch (eventData.type) {
|
|
155
|
+
case 'resource.created':
|
|
156
|
+
case 'resource.cloned': {
|
|
157
|
+
return { exact: eventData.payload.name, isQuoted: false, isTag: false };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Unified annotation events
|
|
161
|
+
case 'annotation.body.updated': {
|
|
162
|
+
// Find current annotation to get its text
|
|
163
|
+
// payload.annotationId is just the UUID, but annotation.id is the full URI
|
|
164
|
+
const annotation = annotations.find(a =>
|
|
165
|
+
a.id.endsWith(`/annotations/${eventData.payload.annotationId}`)
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
if (annotation?.target) {
|
|
169
|
+
try {
|
|
170
|
+
const targetSelector = getTargetSelector(annotation.target);
|
|
171
|
+
const exact = getExactText(targetSelector);
|
|
172
|
+
if (exact) {
|
|
173
|
+
return { exact: truncateText(exact), isQuoted: true, isTag: false };
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// If selector parsing fails, continue to return null
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
case 'annotation.removed': {
|
|
183
|
+
// Find the original annotation.added event to get the text
|
|
184
|
+
// payload.annotationId is just the UUID, but annotation.id in the added event is the full URI
|
|
185
|
+
const addedEvent = allEvents.find(e =>
|
|
186
|
+
e.event.type === 'annotation.added' &&
|
|
187
|
+
e.event.payload.annotation.id.endsWith(`/annotations/${eventData.payload.annotationId}`)
|
|
188
|
+
);
|
|
189
|
+
if (addedEvent && addedEvent.event.type === 'annotation.added') {
|
|
190
|
+
try {
|
|
191
|
+
const target = addedEvent.event.payload.annotation.target;
|
|
192
|
+
if (typeof target !== 'string' && target.selector) {
|
|
193
|
+
const exact = getExactText(target.selector);
|
|
194
|
+
if (exact) {
|
|
195
|
+
return { exact: truncateText(exact), isQuoted: true, isTag: false };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
// If selector parsing fails, return null
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
case 'annotation.added': {
|
|
206
|
+
// New unified event structure - annotation is in payload
|
|
207
|
+
try {
|
|
208
|
+
const target = eventData.payload.annotation.target;
|
|
209
|
+
if (typeof target !== 'string' && target.selector) {
|
|
210
|
+
const exact = getExactText(target.selector);
|
|
211
|
+
if (exact) {
|
|
212
|
+
return { exact: truncateText(exact), isQuoted: true, isTag: false };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// If selector parsing fails, return null
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
case 'entitytag.added':
|
|
222
|
+
case 'entitytag.removed': {
|
|
223
|
+
return { exact: eventData.payload.entityType, isQuoted: false, isTag: true };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
case 'job.completed': {
|
|
227
|
+
// Find the annotation that was used to generate the resource
|
|
228
|
+
if (eventData.payload.annotationUri) {
|
|
229
|
+
const annotation = annotations.find(a =>
|
|
230
|
+
a.id === eventData.payload.annotationUri
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (annotation?.target) {
|
|
234
|
+
try {
|
|
235
|
+
const targetSelector = getTargetSelector(annotation.target);
|
|
236
|
+
const exact = getExactText(targetSelector);
|
|
237
|
+
if (exact) {
|
|
238
|
+
return { exact: truncateText(exact), isQuoted: true, isTag: false };
|
|
239
|
+
}
|
|
240
|
+
} catch {
|
|
241
|
+
// If selector parsing fails, continue to return null
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
case 'job.started':
|
|
249
|
+
case 'job.progress':
|
|
250
|
+
case 'job.failed':
|
|
251
|
+
case 'representation.added':
|
|
252
|
+
case 'representation.removed':
|
|
253
|
+
return null;
|
|
254
|
+
|
|
255
|
+
default:
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get entity types from event payload
|
|
262
|
+
*/
|
|
263
|
+
export function getEventEntityTypes(event: StoredEvent): string[] {
|
|
264
|
+
const eventData = event.event;
|
|
265
|
+
|
|
266
|
+
if (eventData.type === 'annotation.added') {
|
|
267
|
+
const motivation = eventData.payload.annotation.motivation;
|
|
268
|
+
const body = eventData.payload.annotation.body;
|
|
269
|
+
if (motivation === 'linking' && body && 'entityTypes' in body) {
|
|
270
|
+
return (body as any).entityTypes ?? [];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Resource creation details
|
|
279
|
+
*/
|
|
280
|
+
export interface ResourceCreationDetails {
|
|
281
|
+
type: 'created' | 'cloned';
|
|
282
|
+
method: string;
|
|
283
|
+
userId?: string;
|
|
284
|
+
sourceDocId?: string; // For cloned resources
|
|
285
|
+
parentResourceId?: string;
|
|
286
|
+
metadata?: Record<string, any>;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get resource creation details from event
|
|
291
|
+
*/
|
|
292
|
+
export function getResourceCreationDetails(event: StoredEvent): ResourceCreationDetails | null {
|
|
293
|
+
const eventData = event.event;
|
|
294
|
+
|
|
295
|
+
if (eventData.type === 'resource.created') {
|
|
296
|
+
return {
|
|
297
|
+
type: 'created',
|
|
298
|
+
method: eventData.payload.creationMethod || 'unknown',
|
|
299
|
+
userId: eventData.userId,
|
|
300
|
+
metadata: undefined,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (eventData.type === 'resource.cloned') {
|
|
305
|
+
return {
|
|
306
|
+
type: 'cloned',
|
|
307
|
+
method: eventData.payload.creationMethod || 'clone',
|
|
308
|
+
userId: eventData.userId,
|
|
309
|
+
sourceDocId: eventData.payload.parentResourceId,
|
|
310
|
+
parentResourceId: eventData.payload.parentResourceId,
|
|
311
|
+
metadata: undefined,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react';
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
+
import { useMakeMeaningEvents } from '../../../contexts/MakeMeaningEventBusContext';
|
|
5
6
|
import type { components, Selector } from '@semiont/api-client';
|
|
6
7
|
import { AssessmentEntry } from './AssessmentEntry';
|
|
7
8
|
import { useAnnotationPanel } from '../../../hooks/useAnnotationPanel';
|
|
@@ -67,6 +68,7 @@ export function AssessmentPanel({
|
|
|
67
68
|
annotateMode = true,
|
|
68
69
|
}: AssessmentPanelProps) {
|
|
69
70
|
const t = useTranslations('AssessmentPanel');
|
|
71
|
+
const eventBus = useMakeMeaningEvents();
|
|
70
72
|
const [newAssessmentText, setNewAssessmentText] = useState('');
|
|
71
73
|
|
|
72
74
|
const { sortedAnnotations, containerRef, handleAnnotationRef } =
|
|
@@ -79,6 +81,21 @@ export function AssessmentPanel({
|
|
|
79
81
|
}
|
|
80
82
|
};
|
|
81
83
|
|
|
84
|
+
// Escape key handler for cancelling pending annotation
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!pendingAnnotation) return;
|
|
87
|
+
|
|
88
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
89
|
+
if (e.key === 'Escape') {
|
|
90
|
+
eventBus.emit('ui:annotation:cancel-pending');
|
|
91
|
+
setNewAssessmentText('');
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
document.addEventListener('keydown', handleEscape);
|
|
96
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
97
|
+
}, [pendingAnnotation, eventBus]);
|
|
98
|
+
|
|
82
99
|
return (
|
|
83
100
|
<div className="semiont-panel">
|
|
84
101
|
<PanelHeader annotationType="assessment" count={annotations.length} title={t('title')} />
|
|
@@ -109,14 +126,25 @@ export function AssessmentPanel({
|
|
|
109
126
|
<span className="semiont-annotation-prompt__char-count">
|
|
110
127
|
{newAssessmentText.length}/2000
|
|
111
128
|
</span>
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
129
|
+
<div className="semiont-annotation-prompt__actions">
|
|
130
|
+
<button
|
|
131
|
+
onClick={() => {
|
|
132
|
+
eventBus.emit('ui:annotation:cancel-pending');
|
|
133
|
+
setNewAssessmentText('');
|
|
134
|
+
}}
|
|
135
|
+
className="semiont-button semiont-button--secondary"
|
|
136
|
+
data-type="assessment"
|
|
137
|
+
>
|
|
138
|
+
{t('cancel')}
|
|
139
|
+
</button>
|
|
140
|
+
<button
|
|
141
|
+
onClick={handleSaveNewAssessment}
|
|
142
|
+
className="semiont-button semiont-button--primary"
|
|
143
|
+
data-type="assessment"
|
|
144
|
+
>
|
|
145
|
+
{t('save')}
|
|
146
|
+
</button>
|
|
147
|
+
</div>
|
|
120
148
|
</div>
|
|
121
149
|
</div>
|
|
122
150
|
)}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useMemo } from 'react';
|
|
4
3
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
4
|
import './CollaborationPanel.css';
|
|
6
5
|
|
|
@@ -18,9 +17,10 @@ export function CollaborationPanel({
|
|
|
18
17
|
const t = useTranslations('CollaborationPanel');
|
|
19
18
|
|
|
20
19
|
// Format last sync time
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
let lastSyncText: string;
|
|
21
|
+
if (!lastEventTimestamp) {
|
|
22
|
+
lastSyncText = t('noActivity');
|
|
23
|
+
} else {
|
|
24
24
|
const now = new Date();
|
|
25
25
|
const eventTime = new Date(lastEventTimestamp);
|
|
26
26
|
const diffMs = now.getTime() - eventTime.getTime();
|
|
@@ -28,15 +28,22 @@ export function CollaborationPanel({
|
|
|
28
28
|
const diffMins = Math.floor(diffSecs / 60);
|
|
29
29
|
const diffHours = Math.floor(diffMins / 60);
|
|
30
30
|
|
|
31
|
-
if (diffSecs < 10)
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
if (diffSecs < 10) {
|
|
32
|
+
lastSyncText = t('justNow');
|
|
33
|
+
} else if (diffSecs < 60) {
|
|
34
|
+
lastSyncText = t('secondsAgo', { count: diffSecs });
|
|
35
|
+
} else if (diffMins === 1) {
|
|
36
|
+
lastSyncText = t('minuteAgo');
|
|
37
|
+
} else if (diffMins < 60) {
|
|
38
|
+
lastSyncText = t('minutesAgo', { count: diffMins });
|
|
39
|
+
} else if (diffHours === 1) {
|
|
40
|
+
lastSyncText = t('hourAgo');
|
|
41
|
+
} else if (diffHours < 24) {
|
|
42
|
+
lastSyncText = t('hoursAgo', { count: diffHours });
|
|
43
|
+
} else {
|
|
44
|
+
lastSyncText = eventTime.toLocaleDateString();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
40
47
|
|
|
41
48
|
return (
|
|
42
49
|
<div className="semiont-collaboration-panel">
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react';
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
+
import { useMakeMeaningEvents } from '../../../contexts/MakeMeaningEventBusContext';
|
|
5
6
|
import type { components, Selector } from '@semiont/api-client';
|
|
6
7
|
import { CommentEntry } from './CommentEntry';
|
|
7
8
|
import { useAnnotationPanel } from '../../../hooks/useAnnotationPanel';
|
|
@@ -67,6 +68,7 @@ export function CommentsPanel({
|
|
|
67
68
|
detectionProgress,
|
|
68
69
|
}: CommentsPanelProps) {
|
|
69
70
|
const t = useTranslations('CommentsPanel');
|
|
71
|
+
const eventBus = useMakeMeaningEvents();
|
|
70
72
|
const [newCommentText, setNewCommentText] = useState('');
|
|
71
73
|
|
|
72
74
|
const { sortedAnnotations, containerRef, handleAnnotationRef } =
|
|
@@ -79,6 +81,21 @@ export function CommentsPanel({
|
|
|
79
81
|
}
|
|
80
82
|
};
|
|
81
83
|
|
|
84
|
+
// Escape key handler for cancelling pending annotation
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!pendingAnnotation) return;
|
|
87
|
+
|
|
88
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
89
|
+
if (e.key === 'Escape') {
|
|
90
|
+
eventBus.emit('ui:annotation:cancel-pending');
|
|
91
|
+
setNewCommentText('');
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
document.addEventListener('keydown', handleEscape);
|
|
96
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
97
|
+
}, [pendingAnnotation, eventBus]);
|
|
98
|
+
|
|
82
99
|
return (
|
|
83
100
|
<div className="semiont-panel">
|
|
84
101
|
<PanelHeader annotationType="comment" count={annotations.length} title={t('title')} />
|
|
@@ -109,14 +126,26 @@ export function CommentsPanel({
|
|
|
109
126
|
<span className="semiont-annotation-prompt__char-count">
|
|
110
127
|
{newCommentText.length}/2000
|
|
111
128
|
</span>
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
129
|
+
<div className="semiont-annotation-prompt__actions">
|
|
130
|
+
<button
|
|
131
|
+
onClick={() => {
|
|
132
|
+
eventBus.emit('ui:annotation:cancel-pending');
|
|
133
|
+
setNewCommentText('');
|
|
134
|
+
}}
|
|
135
|
+
className="semiont-button semiont-button--secondary"
|
|
136
|
+
data-type="comment"
|
|
137
|
+
>
|
|
138
|
+
{t('cancel')}
|
|
139
|
+
</button>
|
|
140
|
+
<button
|
|
141
|
+
onClick={handleSaveNewComment}
|
|
142
|
+
disabled={!newCommentText.trim()}
|
|
143
|
+
className="semiont-button semiont-button--primary"
|
|
144
|
+
data-type="comment"
|
|
145
|
+
>
|
|
146
|
+
{t('save')}
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
120
149
|
</div>
|
|
121
150
|
</div>
|
|
122
151
|
)}
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useRef, useEffect } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
+
import { useMakeMeaningEvents } from '../../../contexts/MakeMeaningEventBusContext';
|
|
5
6
|
import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
|
|
6
7
|
import { DetectionProgressWidget } from '../../DetectionProgressWidget';
|
|
7
8
|
import { ReferenceEntry } from './ReferenceEntry';
|
|
8
9
|
import type { components, paths, Selector } from '@semiont/api-client';
|
|
9
10
|
import { useAnnotationPanel } from '../../../hooks/useAnnotationPanel';
|
|
10
11
|
import { PanelHeader } from './PanelHeader';
|
|
11
|
-
import { supportsDetection } from '../../../lib/resource-utils';
|
|
12
12
|
import './ReferencesPanel.css';
|
|
13
13
|
|
|
14
14
|
type Annotation = components['schemas']['Annotation'];
|
|
@@ -66,7 +66,6 @@ interface Props {
|
|
|
66
66
|
onGenerateDocument?: (referenceId: string, options: { title: string; prompt?: string }) => void;
|
|
67
67
|
onCreateDocument?: (annotationUri: string, title: string, entityTypes: string[]) => void;
|
|
68
68
|
generatingReferenceId?: string | null;
|
|
69
|
-
mediaType?: string | undefined;
|
|
70
69
|
referencedBy?: ReferencedBy[];
|
|
71
70
|
referencedByLoading?: boolean;
|
|
72
71
|
pendingAnnotation: PendingAnnotation | null;
|
|
@@ -91,13 +90,13 @@ export function ReferencesPanel({
|
|
|
91
90
|
onGenerateDocument,
|
|
92
91
|
onCreateDocument,
|
|
93
92
|
generatingReferenceId,
|
|
94
|
-
mediaType,
|
|
95
93
|
referencedBy = [],
|
|
96
94
|
referencedByLoading = false,
|
|
97
95
|
pendingAnnotation,
|
|
98
96
|
}: Props) {
|
|
99
97
|
const t = useTranslations('DetectPanel');
|
|
100
98
|
const tRef = useTranslations('ReferencesPanel');
|
|
99
|
+
const eventBus = useMakeMeaningEvents();
|
|
101
100
|
const [selectedEntityTypes, setSelectedEntityTypes] = useState<string[]>([]);
|
|
102
101
|
const [lastDetectionLog, setLastDetectionLog] = useState<DetectionLog[] | null>(null);
|
|
103
102
|
const [pendingEntityTypes, setPendingEntityTypes] = useState<string[]>([]);
|
|
@@ -119,9 +118,6 @@ export function ReferencesPanel({
|
|
|
119
118
|
const { sortedAnnotations, containerRef, handleAnnotationRef } =
|
|
120
119
|
useAnnotationPanel(annotations, hoveredAnnotationId);
|
|
121
120
|
|
|
122
|
-
// Check if detection is supported for this media type
|
|
123
|
-
const isTextResource = supportsDetection(mediaType);
|
|
124
|
-
|
|
125
121
|
// Clear log when starting new detection
|
|
126
122
|
const handleDetect = () => {
|
|
127
123
|
if (!onDetect) return;
|
|
@@ -166,6 +162,21 @@ export function ReferencesPanel({
|
|
|
166
162
|
setPendingEntityTypes([]);
|
|
167
163
|
};
|
|
168
164
|
|
|
165
|
+
// Escape key handler for cancelling pending annotation
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (!pendingAnnotation) return;
|
|
168
|
+
|
|
169
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
170
|
+
if (e.key === 'Escape') {
|
|
171
|
+
eventBus.emit('ui:annotation:cancel-pending');
|
|
172
|
+
setPendingEntityTypes([]);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
document.addEventListener('keydown', handleEscape);
|
|
177
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
178
|
+
}, [pendingAnnotation, eventBus]);
|
|
179
|
+
|
|
169
180
|
return (
|
|
170
181
|
<div className="semiont-panel">
|
|
171
182
|
<PanelHeader annotationType="reference" count={annotations.length} title={tRef('referencesTitle')} />
|
|
@@ -205,20 +216,34 @@ export function ReferencesPanel({
|
|
|
205
216
|
</div>
|
|
206
217
|
)}
|
|
207
218
|
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
219
|
+
<div className="semiont-annotation-prompt__footer">
|
|
220
|
+
<div className="semiont-annotation-prompt__actions">
|
|
221
|
+
<button
|
|
222
|
+
onClick={() => {
|
|
223
|
+
eventBus.emit('ui:annotation:cancel-pending');
|
|
224
|
+
setPendingEntityTypes([]);
|
|
225
|
+
}}
|
|
226
|
+
className="semiont-button semiont-button--secondary"
|
|
227
|
+
data-type="reference"
|
|
228
|
+
>
|
|
229
|
+
{tRef('cancel')}
|
|
230
|
+
</button>
|
|
231
|
+
<button
|
|
232
|
+
onClick={handleCreateReference}
|
|
233
|
+
className="semiont-button semiont-button--primary"
|
|
234
|
+
data-type="reference"
|
|
235
|
+
>
|
|
236
|
+
🔗 {tRef('createReference')}
|
|
237
|
+
</button>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
215
240
|
</div>
|
|
216
241
|
)}
|
|
217
242
|
|
|
218
243
|
{/* Scrollable content area */}
|
|
219
244
|
<div ref={containerRef} className="semiont-panel__content">
|
|
220
245
|
{/* Detection Section - only in Annotate mode and for text resources */}
|
|
221
|
-
{annotateMode &&
|
|
246
|
+
{annotateMode && onDetect && (
|
|
222
247
|
<div className="semiont-panel__section">
|
|
223
248
|
<button
|
|
224
249
|
onClick={() => setIsDetectExpanded(!isDetectExpanded)}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useMemo } from 'react';
|
|
4
3
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
4
|
import type { components } from '@semiont/api-client';
|
|
6
5
|
import { isBodyResolved } from '@semiont/api-client';
|
|
@@ -27,28 +26,19 @@ export function StatisticsPanel({
|
|
|
27
26
|
const t = useTranslations('StatisticsPanel');
|
|
28
27
|
|
|
29
28
|
// Count stub vs resolved references
|
|
30
|
-
const stubCount =
|
|
31
|
-
|
|
32
|
-
[references]
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
const resolvedCount = useMemo(
|
|
36
|
-
() => references.filter((r) => isBodyResolved(r.body)).length,
|
|
37
|
-
[references]
|
|
38
|
-
);
|
|
29
|
+
const stubCount = references.filter((r) => !isBodyResolved(r.body)).length;
|
|
30
|
+
const resolvedCount = references.filter((r) => isBodyResolved(r.body)).length;
|
|
39
31
|
|
|
40
32
|
// Count entity types from references (at annotation level)
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
entityTypeCounts.set(type, (entityTypeCounts.get(type) || 0) + 1);
|
|
47
|
-
});
|
|
33
|
+
const entityTypeCounts = new Map<string, number>();
|
|
34
|
+
references.forEach((ref) => {
|
|
35
|
+
const entityTypes = getEntityTypes(ref);
|
|
36
|
+
entityTypes.forEach((type: string) => {
|
|
37
|
+
entityTypeCounts.set(type, (entityTypeCounts.get(type) || 0) + 1);
|
|
48
38
|
});
|
|
39
|
+
});
|
|
49
40
|
|
|
50
|
-
|
|
51
|
-
}, [references]);
|
|
41
|
+
const entityTypesList = Array.from(entityTypeCounts.entries()).sort((a, b) => b[1] - a[1]); // Sort by count descending
|
|
52
42
|
|
|
53
43
|
return (
|
|
54
44
|
<div className="semiont-statistics-panel">
|