@semiont/react-ui 0.2.33-build.80 → 0.2.33-build.81
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/{EventBusContext-7GvDyO0d.d.mts → EventBusContext-CJjL_cCf.d.mts} +67 -19
- package/dist/{chunk-ZR4ZV2LY.mjs → chunk-QB52Q7EQ.mjs} +7 -7
- package/dist/chunk-QB52Q7EQ.mjs.map +1 -0
- package/dist/index.d.mts +166 -199
- package/dist/index.mjs +1486 -1599
- package/dist/index.mjs.map +1 -1
- package/dist/test-utils.d.mts +2 -2
- package/dist/test-utils.mjs +1 -1
- package/package.json +1 -1
- package/src/components/LiveRegion.tsx +18 -18
- package/src/components/SessionExpiryBanner.tsx +2 -3
- package/src/components/SessionTimer.tsx +2 -2
- package/src/components/__tests__/SessionTimer.test.tsx +27 -27
- package/src/components/resource/panels/AssessmentPanel.tsx +2 -2
- package/src/components/resource/panels/DetectSection.tsx +13 -7
- package/src/components/resource/panels/ReferenceEntry.tsx +1 -1
- package/src/components/resource/panels/TaggingPanel.tsx +2 -3
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +1 -1
- package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +16 -13
- package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +1 -1
- package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +13 -13
- package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +5 -5
- package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +5 -6
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +11 -12
- package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +3 -3
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +130 -93
- package/dist/chunk-ZR4ZV2LY.mjs.map +0 -1
|
@@ -42,7 +42,7 @@ describe('Detection Progress Dismissal Bug', () => {
|
|
|
42
42
|
close: vi.fn(),
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
vi.spyOn(SSEClient.prototype, '
|
|
45
|
+
vi.spyOn(SSEClient.prototype, 'detectReferences').mockReturnValue(mockStream);
|
|
46
46
|
vi.spyOn(SSEClient.prototype, 'detectHighlights').mockReturnValue(mockStream);
|
|
47
47
|
vi.spyOn(SSEClient.prototype, 'detectComments').mockReturnValue(mockStream);
|
|
48
48
|
vi.spyOn(SSEClient.prototype, 'detectAssessments').mockReturnValue(mockStream);
|
|
@@ -231,11 +231,11 @@ describe('Detection Progress Dismissal Bug', () => {
|
|
|
231
231
|
});
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
-
it('FIXED:
|
|
234
|
+
it('FIXED: useResolutionFlow now forwards final completion chunk data', async () => {
|
|
235
235
|
/**
|
|
236
|
-
* This test verifies the fix for the
|
|
236
|
+
* This test verifies the fix for the useResolutionFlow bug.
|
|
237
237
|
*
|
|
238
|
-
* FIX:
|
|
238
|
+
* FIX: useResolutionFlow.ts stream.onComplete(finalChunk) now emits detection:progress
|
|
239
239
|
* with the final chunk data BEFORE emitting detection:complete.
|
|
240
240
|
*
|
|
241
241
|
* This ensures the UI can display the final completion message with status:'complete'.
|
|
@@ -300,7 +300,7 @@ describe('Detection Progress Dismissal Bug', () => {
|
|
|
300
300
|
});
|
|
301
301
|
|
|
302
302
|
// Simulate backend sending final chunk to stream.onComplete(finalChunk)
|
|
303
|
-
//
|
|
303
|
+
// useResolutionFlow should forward this as detection:progress
|
|
304
304
|
act(() => {
|
|
305
305
|
onCompleteCallback?.({
|
|
306
306
|
status: 'complete',
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
* Tests the COMPLETE generation flow with real component composition:
|
|
5
5
|
* - EventBusProvider (REAL)
|
|
6
6
|
* - ApiClientProvider (REAL, with MOCKED client)
|
|
7
|
-
* - useGenerationFlow (REAL)
|
|
8
|
-
* -
|
|
9
|
-
* - useEventOperations (REAL)
|
|
7
|
+
* - useGenerationFlow (REAL, with inlined progress state)
|
|
8
|
+
* - useResolutionFlow (REAL)
|
|
10
9
|
* - useEventSubscriptions (REAL)
|
|
11
10
|
*
|
|
12
11
|
* This test focuses on ARCHITECTURE and EVENT WIRING:
|
|
@@ -26,7 +25,7 @@ import { useGenerationFlow } from '../../../hooks/useGenerationFlow';
|
|
|
26
25
|
import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../../contexts/EventBusContext';
|
|
27
26
|
import { ApiClientProvider, useApiClient } from '../../../contexts/ApiClientContext';
|
|
28
27
|
import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
|
|
29
|
-
import {
|
|
28
|
+
import { useResolutionFlow } from '../../../contexts/useResolutionFlow';
|
|
30
29
|
import { SSEClient } from '@semiont/api-client';
|
|
31
30
|
import type { SemiontApiClient, ResourceUri, AnnotationUri } from '@semiont/api-client';
|
|
32
31
|
import { resourceUri, annotationUri } from '@semiont/api-client';
|
|
@@ -418,8 +417,8 @@ function renderGenerationFlow(
|
|
|
418
417
|
eventBusInstance = useEventBus();
|
|
419
418
|
const client = useApiClient();
|
|
420
419
|
|
|
421
|
-
// Set up
|
|
422
|
-
|
|
420
|
+
// Set up resolution flow (annotation:update-body, reference:link)
|
|
421
|
+
useResolutionFlow(eventBusInstance, {
|
|
423
422
|
client: client as SemiontApiClient,
|
|
424
423
|
resourceUri: testResourceUri,
|
|
425
424
|
});
|
|
@@ -15,6 +15,7 @@ import { EventBusProvider, resetEventBusForTesting } from '../../../contexts/Eve
|
|
|
15
15
|
import { ApiClientProvider } from '../../../contexts/ApiClientContext';
|
|
16
16
|
import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
|
|
17
17
|
import { ToastProvider } from '../../../components/Toast';
|
|
18
|
+
import { ThemeProvider } from '../../../contexts/ThemeContext';
|
|
18
19
|
|
|
19
20
|
// jsdom doesn't implement window.matchMedia — mock it for useTheme
|
|
20
21
|
Object.defineProperty(window, 'matchMedia', {
|
|
@@ -76,11 +77,7 @@ vi.mock('@semiont/react-ui', async () => {
|
|
|
76
77
|
JsonLdPanel: () => <div data-testid="jsonld-panel">JSON-LD</div>,
|
|
77
78
|
ErrorBoundary: ({ children }: any) => children,
|
|
78
79
|
createCancelDetectionHandler: () => vi.fn(),
|
|
79
|
-
|
|
80
|
-
progress: null,
|
|
81
|
-
clearProgress: vi.fn(),
|
|
82
|
-
}),
|
|
83
|
-
useDebouncedCallback: (fn: any) => fn,
|
|
80
|
+
useDebouncedCallback: (fn: any) => fn,
|
|
84
81
|
supportsDetection: () => false,
|
|
85
82
|
MakeMeaningEventBusProvider: ({ children }: any) => children,
|
|
86
83
|
useResourceLoadingAnnouncements: () => ({
|
|
@@ -165,13 +162,15 @@ const createMockProps = (overrides?: Partial<ResourceViewerPageProps>): Resource
|
|
|
165
162
|
// Test wrapper to provide all required providers
|
|
166
163
|
const renderWithProviders = (ui: React.ReactElement) => {
|
|
167
164
|
return render(
|
|
168
|
-
<
|
|
169
|
-
<
|
|
170
|
-
<
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
165
|
+
<ThemeProvider>
|
|
166
|
+
<ToastProvider>
|
|
167
|
+
<AuthTokenProvider token={null}>
|
|
168
|
+
<ApiClientProvider baseUrl="http://localhost:4000">
|
|
169
|
+
<EventBusProvider>{ui}</EventBusProvider>
|
|
170
|
+
</ApiClientProvider>
|
|
171
|
+
</AuthTokenProvider>
|
|
172
|
+
</ToastProvider>
|
|
173
|
+
</ThemeProvider>
|
|
175
174
|
);
|
|
176
175
|
};
|
|
177
176
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Layer 3 Integration Test: Detection Progress Flow UI/UX
|
|
3
3
|
*
|
|
4
|
-
* Tests the complete data flow from UI → EventBus →
|
|
4
|
+
* Tests the complete data flow from UI → EventBus → useResolutionFlow → SSE (mocked)
|
|
5
5
|
*
|
|
6
6
|
* This test uses COMPOSITION instead of mocking:
|
|
7
7
|
* - Real React components composed together (useDetectionFlow + HighlightPanel + DetectSection)
|
|
8
8
|
* - Real EventBus (mitt) passed via context
|
|
9
|
-
* - Real
|
|
9
|
+
* - Real useResolutionFlow hook with mock API client passed as prop
|
|
10
10
|
* - Mock SSE stream (simulated API responses) provided via composition
|
|
11
11
|
*
|
|
12
12
|
* This test focuses on USER EXPERIENCE:
|
|
@@ -145,7 +145,7 @@ describe('Detection Progress Flow Integration (Layer 3)', () => {
|
|
|
145
145
|
vi.spyOn(SSEClient.prototype, 'detectHighlights').mockReturnValue(mockStream as any);
|
|
146
146
|
vi.spyOn(SSEClient.prototype, 'detectAssessments').mockReturnValue(mockStream as any);
|
|
147
147
|
vi.spyOn(SSEClient.prototype, 'detectComments').mockReturnValue(mockStream as any);
|
|
148
|
-
vi.spyOn(SSEClient.prototype, '
|
|
148
|
+
vi.spyOn(SSEClient.prototype, 'detectReferences').mockReturnValue(mockStream as any);
|
|
149
149
|
|
|
150
150
|
mockAnnotations = [];
|
|
151
151
|
});
|
|
@@ -25,7 +25,7 @@ import { QUERY_KEYS } from '../../../lib/query-keys';
|
|
|
25
25
|
import { useResources, useEntityTypes } from '../../../lib/api-hooks';
|
|
26
26
|
import { useResourceContent } from '../../../hooks/useResourceContent';
|
|
27
27
|
import { useToast } from '../../../components/Toast';
|
|
28
|
-
import { useTheme } from '../../../
|
|
28
|
+
import { useTheme } from '../../../contexts/ThemeContext';
|
|
29
29
|
import { useLineNumbers } from '../../../hooks/useLineNumbers';
|
|
30
30
|
import { useResourceEvents } from '../../../hooks/useResourceEvents';
|
|
31
31
|
import { useDebouncedCallback } from '../../../hooks/useDebounce';
|
|
@@ -34,10 +34,12 @@ import { useOpenResources } from '../../../contexts/OpenResourcesContext';
|
|
|
34
34
|
import { useEventBus } from '../../../contexts/EventBusContext';
|
|
35
35
|
import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
|
|
36
36
|
import { useResourceAnnotations } from '../../../contexts/ResourceAnnotationsContext';
|
|
37
|
+
import { useApiClient } from '../../../contexts/ApiClientContext';
|
|
38
|
+
import { useResolutionFlow } from '../../../contexts/useResolutionFlow';
|
|
37
39
|
import { useDetectionFlow } from '../../../hooks/useDetectionFlow';
|
|
38
40
|
import { usePanelNavigation } from '../../../hooks/usePanelNavigation';
|
|
39
|
-
import { useAnnotationFlow } from '../../../hooks/useAnnotationFlow';
|
|
40
41
|
import { useGenerationFlow } from '../../../hooks/useGenerationFlow';
|
|
42
|
+
import { useContextRetrievalFlow } from '../../../hooks/useContextRetrievalFlow';
|
|
41
43
|
|
|
42
44
|
type SemiontResource = components['schemas']['ResourceDescriptor'];
|
|
43
45
|
type Annotation = components['schemas']['Annotation'];
|
|
@@ -90,6 +92,28 @@ export interface ResourceViewerPageProps {
|
|
|
90
92
|
* ResourceViewerPage - Main component
|
|
91
93
|
*
|
|
92
94
|
* Uses hooks directly (NO containers, NO render props, NO ResourceViewerPageContent wrapper)
|
|
95
|
+
*
|
|
96
|
+
* @emits navigation:router-push - Navigate to a resource or filtered view
|
|
97
|
+
* @emits annotation:sparkle - Trigger sparkle animation on an annotation
|
|
98
|
+
* @emits annotation:update-body - Update annotation body content
|
|
99
|
+
* @subscribes resource:archive - Archive the current resource
|
|
100
|
+
* @subscribes resource:unarchive - Unarchive the current resource
|
|
101
|
+
* @subscribes resource:clone - Clone the current resource
|
|
102
|
+
* @subscribes annotation:sparkle - Trigger sparkle animation
|
|
103
|
+
* @subscribes annotation:created - Annotation was created
|
|
104
|
+
* @subscribes annotation:deleted - Annotation was deleted
|
|
105
|
+
* @subscribes annotation:create-failed - Annotation creation failed
|
|
106
|
+
* @subscribes annotation:delete-failed - Annotation deletion failed
|
|
107
|
+
* @subscribes annotation:body-updated - Annotation body was updated
|
|
108
|
+
* @subscribes annotation:body-update-failed - Annotation body update failed
|
|
109
|
+
* @subscribes settings:theme-changed - UI theme changed
|
|
110
|
+
* @subscribes settings:line-numbers-toggled - Line numbers display toggled
|
|
111
|
+
* @subscribes detection:complete - Detection completed
|
|
112
|
+
* @subscribes detection:failed - Detection failed
|
|
113
|
+
* @subscribes generation:complete - Generation completed
|
|
114
|
+
* @subscribes generation:failed - Generation failed
|
|
115
|
+
* @subscribes navigation:reference-navigate - Navigate to a referenced document
|
|
116
|
+
* @subscribes navigation:entity-type-clicked - Navigate filtered by entity type
|
|
93
117
|
*/
|
|
94
118
|
export function ResourceViewerPage({
|
|
95
119
|
resource,
|
|
@@ -105,6 +129,7 @@ export function ResourceViewerPage({
|
|
|
105
129
|
}: ResourceViewerPageProps) {
|
|
106
130
|
// Get unified event bus for subscribing to UI events
|
|
107
131
|
const eventBus = useEventBus();
|
|
132
|
+
const client = useApiClient();
|
|
108
133
|
const queryClient = useQueryClient();
|
|
109
134
|
|
|
110
135
|
// UI state hooks
|
|
@@ -134,26 +159,24 @@ export function ResourceViewerPage({
|
|
|
134
159
|
const allEntityTypes = (entityTypesData as { entityTypes: string[] } | undefined)?.entityTypes || [];
|
|
135
160
|
|
|
136
161
|
// Flow state hooks (NO CONTAINERS)
|
|
137
|
-
const { detectingMotivation, detectionProgress } = useDetectionFlow(rUri);
|
|
162
|
+
const { detectingMotivation, detectionProgress, pendingAnnotation, hoveredAnnotationId } = useDetectionFlow(rUri);
|
|
138
163
|
const { activePanel, scrollToAnnotationId, panelInitialTab, onScrollCompleted } = usePanelNavigation();
|
|
139
|
-
const {
|
|
164
|
+
const { searchModalOpen, pendingReferenceId, onCloseSearchModal } = useResolutionFlow(eventBus, { client, resourceUri: rUri });
|
|
140
165
|
const {
|
|
141
166
|
generationProgress,
|
|
142
167
|
generationModalOpen,
|
|
143
168
|
generationReferenceId,
|
|
144
169
|
generationDefaultTitle,
|
|
145
|
-
searchModalOpen,
|
|
146
|
-
pendingReferenceId,
|
|
147
170
|
onGenerateDocument,
|
|
148
171
|
onCloseGenerationModal,
|
|
149
|
-
onCloseSearchModal,
|
|
150
172
|
} = useGenerationFlow(locale, rUri.split('/').pop() || '', showSuccess, showError, cacheManager, clearNewAnnotationId);
|
|
173
|
+
const { retrievalContext, retrievalLoading, retrievalError } = useContextRetrievalFlow(eventBus, { client, resourceUri: rUri });
|
|
151
174
|
|
|
152
175
|
// Debounced invalidation for real-time events
|
|
153
176
|
const debouncedInvalidateAnnotations = useDebouncedCallback(
|
|
154
177
|
() => {
|
|
155
|
-
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.
|
|
156
|
-
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.
|
|
178
|
+
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.annotations(rUri) });
|
|
179
|
+
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.events(rUri) });
|
|
157
180
|
},
|
|
158
181
|
500
|
|
159
182
|
);
|
|
@@ -186,7 +209,7 @@ export function ResourceViewerPage({
|
|
|
186
209
|
|
|
187
210
|
onAnnotationBodyUpdated: useCallback((event: any) => {
|
|
188
211
|
// Optimistically update annotations cache with body operations
|
|
189
|
-
queryClient.setQueryData(QUERY_KEYS.
|
|
212
|
+
queryClient.setQueryData(QUERY_KEYS.resources.annotations(rUri), (old: any) => {
|
|
190
213
|
if (!old) return old;
|
|
191
214
|
return {
|
|
192
215
|
...old,
|
|
@@ -222,7 +245,7 @@ export function ResourceViewerPage({
|
|
|
222
245
|
};
|
|
223
246
|
});
|
|
224
247
|
|
|
225
|
-
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.
|
|
248
|
+
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.events(rUri) });
|
|
226
249
|
}, [queryClient, rUri]),
|
|
227
250
|
|
|
228
251
|
// Document status events
|
|
@@ -254,90 +277,103 @@ export function ResourceViewerPage({
|
|
|
254
277
|
}, []),
|
|
255
278
|
});
|
|
256
279
|
|
|
280
|
+
// Event handlers extracted to useCallback (tenet: no inline handlers in useEventSubscriptions)
|
|
281
|
+
const handleResourceArchive = useCallback(async () => {
|
|
282
|
+
try {
|
|
283
|
+
await resources.update.useMutation().mutateAsync({ rUri, data: { archived: true } });
|
|
284
|
+
await refetchDocument();
|
|
285
|
+
showSuccess('Document archived');
|
|
286
|
+
} catch (err) {
|
|
287
|
+
console.error('Failed to archive document:', err);
|
|
288
|
+
showError('Failed to archive document');
|
|
289
|
+
}
|
|
290
|
+
}, [resources.update, rUri, refetchDocument, showSuccess, showError]);
|
|
291
|
+
|
|
292
|
+
const handleResourceUnarchive = useCallback(async () => {
|
|
293
|
+
try {
|
|
294
|
+
await resources.update.useMutation().mutateAsync({ rUri, data: { archived: false } });
|
|
295
|
+
await refetchDocument();
|
|
296
|
+
showSuccess('Document unarchived');
|
|
297
|
+
} catch (err) {
|
|
298
|
+
console.error('Failed to unarchive document:', err);
|
|
299
|
+
showError('Failed to unarchive document');
|
|
300
|
+
}
|
|
301
|
+
}, [resources.update, rUri, refetchDocument, showSuccess, showError]);
|
|
302
|
+
|
|
303
|
+
const handleResourceClone = useCallback(async () => {
|
|
304
|
+
try {
|
|
305
|
+
const result = await resources.generateCloneToken.useMutation().mutateAsync(rUri);
|
|
306
|
+
const token = result.token;
|
|
307
|
+
const cloneUrl = `${typeof window !== 'undefined' ? window.location.origin : ''}/know/clone?token=${token}`;
|
|
308
|
+
await navigator.clipboard.writeText(cloneUrl);
|
|
309
|
+
showSuccess('Clone link copied to clipboard');
|
|
310
|
+
} catch (err) {
|
|
311
|
+
console.error('Failed to generate clone token:', err);
|
|
312
|
+
showError('Failed to generate clone link');
|
|
313
|
+
}
|
|
314
|
+
}, [resources.generateCloneToken, rUri, showSuccess, showError]);
|
|
315
|
+
|
|
316
|
+
const handleAnnotationSparkle = useCallback(({ annotationId }: { annotationId: string }) => {
|
|
317
|
+
triggerSparkleAnimation(annotationId);
|
|
318
|
+
}, [triggerSparkleAnimation]);
|
|
319
|
+
|
|
320
|
+
const handleAnnotationCreated = useCallback(({ annotation }: { annotation: { id: string } }) => {
|
|
321
|
+
triggerSparkleAnimation(annotation.id);
|
|
322
|
+
debouncedInvalidateAnnotations();
|
|
323
|
+
}, [triggerSparkleAnimation, debouncedInvalidateAnnotations]);
|
|
324
|
+
|
|
325
|
+
const handleAnnotationCreateFailed = useCallback(() => showError('Failed to create annotation'), [showError]);
|
|
326
|
+
const handleAnnotationDeleteFailed = useCallback(() => showError('Failed to delete annotation'), [showError]);
|
|
327
|
+
const handleAnnotationBodyUpdated = useCallback(() => {
|
|
328
|
+
// Success - optimistic update already applied via useResourceEvents
|
|
329
|
+
}, []);
|
|
330
|
+
const handleAnnotationBodyUpdateFailed = useCallback(() => showError('Failed to update annotation'), [showError]);
|
|
331
|
+
|
|
332
|
+
const handleSettingsThemeChanged = useCallback(({ theme }: { theme: any }) => setTheme(theme), [setTheme]);
|
|
333
|
+
|
|
334
|
+
const handleDetectionComplete = useCallback(() => {
|
|
335
|
+
showSuccess('Detection complete');
|
|
336
|
+
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.annotations(rUri) });
|
|
337
|
+
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.events(rUri) });
|
|
338
|
+
}, [showSuccess, queryClient, rUri]);
|
|
339
|
+
const handleDetectionFailed = useCallback(() => showError('Detection failed'), [showError]);
|
|
340
|
+
const handleGenerationComplete = useCallback(() => showSuccess('Document generated'), [showSuccess]);
|
|
341
|
+
const handleGenerationFailed = useCallback(() => showError('Failed to generate document'), [showError]);
|
|
342
|
+
|
|
343
|
+
const handleReferenceNavigate = useCallback(({ documentId }: { documentId: string }) => {
|
|
344
|
+
if (routes.resource) {
|
|
345
|
+
const path = routes.resource.replace('[resourceId]', encodeURIComponent(documentId));
|
|
346
|
+
eventBus.emit('navigation:router-push', { path, reason: 'reference-link' });
|
|
347
|
+
}
|
|
348
|
+
}, [routes.resource]); // eventBus is stable singleton - never in deps
|
|
349
|
+
|
|
350
|
+
const handleEntityTypeClicked = useCallback(({ entityType }: { entityType: string }) => {
|
|
351
|
+
if (routes.know) {
|
|
352
|
+
const path = `${routes.know}?entityType=${encodeURIComponent(entityType)}`;
|
|
353
|
+
eventBus.emit('navigation:router-push', { path, reason: 'entity-type-filter' });
|
|
354
|
+
}
|
|
355
|
+
}, [routes.know]); // eventBus is stable singleton - never in deps
|
|
356
|
+
|
|
257
357
|
// Event bus subscriptions (combined into single useEventSubscriptions call to prevent hook ordering issues)
|
|
258
358
|
useEventSubscriptions({
|
|
259
|
-
|
|
260
|
-
'resource:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
data: { archived: true }
|
|
265
|
-
});
|
|
266
|
-
await refetchDocument();
|
|
267
|
-
showSuccess('Document archived');
|
|
268
|
-
} catch (err) {
|
|
269
|
-
console.error('Failed to archive document:', err);
|
|
270
|
-
showError('Failed to archive document');
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
|
-
'resource:unarchive': async () => {
|
|
274
|
-
try {
|
|
275
|
-
await resources.update.useMutation().mutateAsync({
|
|
276
|
-
rUri,
|
|
277
|
-
data: { archived: false }
|
|
278
|
-
});
|
|
279
|
-
await refetchDocument();
|
|
280
|
-
showSuccess('Document unarchived');
|
|
281
|
-
} catch (err) {
|
|
282
|
-
console.error('Failed to unarchive document:', err);
|
|
283
|
-
showError('Failed to unarchive document');
|
|
284
|
-
}
|
|
285
|
-
},
|
|
286
|
-
'resource:clone': async () => {
|
|
287
|
-
try {
|
|
288
|
-
const result = await resources.generateCloneToken.useMutation().mutateAsync(rUri);
|
|
289
|
-
const token = result.token;
|
|
290
|
-
const cloneUrl = `${typeof window !== 'undefined' ? window.location.origin : ''}/know/clone?token=${token}`;
|
|
291
|
-
|
|
292
|
-
await navigator.clipboard.writeText(cloneUrl);
|
|
293
|
-
showSuccess('Clone link copied to clipboard');
|
|
294
|
-
} catch (err) {
|
|
295
|
-
console.error('Failed to generate clone token:', err);
|
|
296
|
-
showError('Failed to generate clone link');
|
|
297
|
-
}
|
|
298
|
-
},
|
|
299
|
-
|
|
300
|
-
// Annotation operations
|
|
301
|
-
'annotation:sparkle': ({ annotationId }) => {
|
|
302
|
-
triggerSparkleAnimation(annotationId);
|
|
303
|
-
},
|
|
304
|
-
'annotation:created': ({ annotation }) => {
|
|
305
|
-
triggerSparkleAnimation(annotation.id);
|
|
306
|
-
debouncedInvalidateAnnotations();
|
|
307
|
-
},
|
|
359
|
+
'resource:archive': handleResourceArchive,
|
|
360
|
+
'resource:unarchive': handleResourceUnarchive,
|
|
361
|
+
'resource:clone': handleResourceClone,
|
|
362
|
+
'annotation:sparkle': handleAnnotationSparkle,
|
|
363
|
+
'annotation:created': handleAnnotationCreated,
|
|
308
364
|
'annotation:deleted': debouncedInvalidateAnnotations,
|
|
309
|
-
'annotation:create-failed':
|
|
310
|
-
'annotation:delete-failed':
|
|
311
|
-
'annotation:body-updated':
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
'annotation:body-update-failed': () => showError('Failed to update annotation'),
|
|
315
|
-
|
|
316
|
-
// Settings
|
|
317
|
-
'settings:theme-changed': ({ theme }) => setTheme(theme),
|
|
365
|
+
'annotation:create-failed': handleAnnotationCreateFailed,
|
|
366
|
+
'annotation:delete-failed': handleAnnotationDeleteFailed,
|
|
367
|
+
'annotation:body-updated': handleAnnotationBodyUpdated,
|
|
368
|
+
'annotation:body-update-failed': handleAnnotationBodyUpdateFailed,
|
|
369
|
+
'settings:theme-changed': handleSettingsThemeChanged,
|
|
318
370
|
'settings:line-numbers-toggled': toggleLineNumbers,
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
'
|
|
322
|
-
'
|
|
323
|
-
'
|
|
324
|
-
'
|
|
325
|
-
|
|
326
|
-
// Navigation
|
|
327
|
-
'navigation:reference-navigate': ({ documentId }: { documentId: string }) => {
|
|
328
|
-
// Navigate to the referenced document
|
|
329
|
-
if (routes.resource) {
|
|
330
|
-
const path = routes.resource.replace('[resourceId]', encodeURIComponent(documentId));
|
|
331
|
-
eventBus.emit('navigation:router-push', { path, reason: 'reference-link' });
|
|
332
|
-
}
|
|
333
|
-
},
|
|
334
|
-
'navigation:entity-type-clicked': ({ entityType }: { entityType: string }) => {
|
|
335
|
-
// Navigate to discovery page filtered by entity type
|
|
336
|
-
if (routes.know) {
|
|
337
|
-
const path = `${routes.know}?entityType=${encodeURIComponent(entityType)}`;
|
|
338
|
-
eventBus.emit('navigation:router-push', { path, reason: 'entity-type-filter' });
|
|
339
|
-
}
|
|
340
|
-
},
|
|
371
|
+
'detection:complete': handleDetectionComplete,
|
|
372
|
+
'detection:failed': handleDetectionFailed,
|
|
373
|
+
'generation:complete': handleGenerationComplete,
|
|
374
|
+
'generation:failed': handleGenerationFailed,
|
|
375
|
+
'navigation:reference-navigate': handleReferenceNavigate,
|
|
376
|
+
'navigation:entity-type-clicked': handleEntityTypeClicked,
|
|
341
377
|
});
|
|
342
378
|
|
|
343
379
|
// Resource loading announcements
|
|
@@ -597,9 +633,10 @@ export function ResourceViewerPage({
|
|
|
597
633
|
onGenerateDocument(generationReferenceId, options);
|
|
598
634
|
}
|
|
599
635
|
}}
|
|
600
|
-
referenceId={generationReferenceId || ''}
|
|
601
|
-
resourceUri={rUri}
|
|
602
636
|
defaultTitle={generationDefaultTitle}
|
|
637
|
+
context={retrievalContext}
|
|
638
|
+
contextLoading={retrievalLoading}
|
|
639
|
+
contextError={retrievalError}
|
|
603
640
|
/>
|
|
604
641
|
</div>
|
|
605
642
|
);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/contexts/ApiClientContext.tsx","../src/contexts/EventBusContext.tsx","../src/contexts/SessionContext.tsx","../src/components/Toast.tsx","../src/contexts/OpenResourcesContext.tsx","../src/contexts/TranslationContext.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, ReactNode, useMemo } from 'react';\nimport { SemiontApiClient, baseUrl } from '@semiont/api-client';\n\nconst ApiClientContext = createContext<SemiontApiClient | undefined>(undefined);\n\nexport interface ApiClientProviderProps {\n baseUrl: string;\n children: ReactNode;\n}\n\n/**\n * Provider for API client - creates a stateless singleton client\n * The client instance never changes (no token dependency)\n * Auth tokens are passed per-request via useAuthToken() in calling code\n */\nexport function ApiClientProvider({\n baseUrl: url,\n children,\n}: ApiClientProviderProps) {\n // Client created once and never recreated (no token dependency)\n const client = useMemo(\n () => new SemiontApiClient({\n baseUrl: baseUrl(url),\n // Use no timeout in test environment to avoid AbortController issues with ky + vitest\n ...(process.env.NODE_ENV !== 'test' && { timeout: 30000 }),\n }),\n [url] // Only baseUrl in deps, token removed\n );\n\n return (\n <ApiClientContext.Provider value={client}>\n {children}\n </ApiClientContext.Provider>\n );\n}\n\n/**\n * Hook to access the stateless API client singleton\n * Must be used within an ApiClientProvider\n * @returns Stateless SemiontApiClient instance\n */\nexport function useApiClient(): SemiontApiClient {\n const context = useContext(ApiClientContext);\n\n if (context === undefined) {\n throw new Error('useApiClient must be used within an ApiClientProvider');\n }\n\n return context;\n}\n","'use client';\n\nimport { createContext, useContext, useMemo, type ReactNode } from 'react';\nimport mitt from 'mitt';\nimport type { ResourceEvent } from '@semiont/core';\nimport type { components, Selector, ResourceUri } from '@semiont/api-client';\n\ntype Annotation = components['schemas']['Annotation'];\ntype Motivation = components['schemas']['Motivation'];\n\ninterface SelectionData {\n exact: string;\n start: number;\n end: number;\n svgSelector?: string;\n fragmentSelector?: string;\n conformsTo?: string;\n prefix?: string;\n suffix?: string;\n}\n\n/**\n * Unified event map for all application events\n *\n * Consolidates events from:\n * - MakeMeaningEventBus (document/annotation operations)\n * - NavigationEventBus (navigation and sidebar UI)\n * - GlobalSettingsEventBus (app-wide settings)\n */\nexport type EventMap = {\n // ===== BACKEND EVENTS (from SSE) =====\n\n // Generic event (all types)\n 'make-meaning:event': ResourceEvent;\n\n // Detection events\n 'detection:started': Extract<ResourceEvent, { type: 'job.started' }>;\n 'detection:progress': Extract<ResourceEvent, { type: 'job.progress' }>;\n 'detection:entity-found': Extract<ResourceEvent, { type: 'annotation.added' }>;\n 'detection:completed': Extract<ResourceEvent, { type: 'job.completed' }>;\n 'detection:failed': Extract<ResourceEvent, { type: 'job.failed' }>;\n\n // Annotation events (backend)\n 'annotation:added': Extract<ResourceEvent, { type: 'annotation.added' }>;\n 'annotation:removed': Extract<ResourceEvent, { type: 'annotation.removed' }>;\n 'annotation:updated': Extract<ResourceEvent, { type: 'annotation.body.updated' }>;\n\n // Entity tag events (backend)\n 'entity-tag:added': Extract<ResourceEvent, { type: 'entitytag.added' }>;\n 'entity-tag:removed': Extract<ResourceEvent, { type: 'entitytag.removed' }>;\n\n // Resource events (backend)\n 'resource:archived': Extract<ResourceEvent, { type: 'resource.archived' }>;\n 'resource:unarchived': Extract<ResourceEvent, { type: 'resource.unarchived' }>;\n\n // ===== USER INTERACTION EVENTS =====\n\n // Selection events (user highlighting text/regions)\n 'selection:comment-requested': SelectionData;\n 'selection:tag-requested': SelectionData;\n 'selection:assessment-requested': SelectionData;\n 'selection:reference-requested': SelectionData;\n\n // Unified annotation request event (all motivations)\n 'annotation:requested': {\n selector: Selector | Selector[];\n motivation: Motivation;\n };\n\n // Annotation interaction events\n 'annotation:cancel-pending': void;\n 'annotation:dom-hover': { annotationId: string | null }; // Raw DOM hover event from resource overlays (internal routing)\n 'annotation:hover': { annotationId: string | null }; // Bidirectional hover: annotation overlay ↔ panel entry\n 'annotation:click': { annotationId: string; motivation: Motivation }; // Click on annotation - includes motivation for panel coordination\n 'annotation:focus': { annotationId: string | null };\n 'annotation:sparkle': { annotationId: string };\n\n // Panel management events\n 'panel:toggle': { panel: string };\n 'panel:open': { panel: string; scrollToAnnotationId?: string; motivation?: string };\n 'panel:close': void;\n\n // View mode events\n 'view:mode-toggled': void;\n\n // Toolbar events (annotation UI controls)\n 'toolbar:selection-changed': { motivation: string | null };\n 'toolbar:click-changed': { action: string };\n 'toolbar:shape-changed': { shape: string };\n\n // Navigation events (sidebar UI)\n 'navigation:sidebar-toggle': void;\n 'navigation:resource-close': { resourceId: string };\n 'navigation:resource-reorder': { oldIndex: number; newIndex: number };\n 'navigation:link-clicked': { href: string; label?: string };\n 'navigation:router-push': { path: string; reason?: string };\n 'navigation:external-navigate': { url: string; resourceId?: string };\n 'navigation:reference-navigate': { documentId: string };\n 'navigation:entity-type-clicked': { entityType: string };\n\n // Settings events (app-wide)\n 'settings:theme-changed': { theme: 'light' | 'dark' | 'system' };\n 'settings:line-numbers-toggled': void;\n 'settings:locale-changed': { locale: string };\n\n // ===== API OPERATION EVENTS =====\n\n // Resource operations\n 'resource:archive': void;\n 'resource:unarchive': void;\n 'resource:clone': void;\n\n // Job control\n 'job:cancel-requested': { jobType: 'detection' | 'generation' };\n\n // Annotation CRUD operations\n 'annotation:create': {\n motivation: Motivation;\n selector: Selector | Selector[];\n body: any[];\n };\n 'annotation:created': { annotation: Annotation };\n 'annotation:create-failed': { error: Error };\n 'annotation:delete': { annotationId: string };\n 'annotation:deleted': { annotationId: string };\n 'annotation:delete-failed': { error: Error };\n 'annotation:update-body': {\n annotationUri: string;\n resourceId: string;\n operations: Array<{\n op: 'add' | 'remove' | 'replace';\n item?: any;\n oldItem?: any;\n newItem?: any;\n }>;\n };\n 'annotation:body-updated': { annotationUri: string };\n 'annotation:body-update-failed': { error: Error };\n\n // Detection operations\n 'detection:start': {\n motivation: Motivation;\n options: {\n instructions?: string;\n tone?: 'neutral' | 'supportive' | 'critical';\n density?: number;\n entityTypes?: string[];\n includeDescriptiveReferences?: boolean;\n schemaId?: string;\n categories?: string[];\n };\n };\n 'detection:complete': { motivation?: Motivation; resourceUri?: ResourceUri; progress?: any };\n 'detection:cancelled': void;\n 'detection:dismiss-progress': void;\n\n // Resource generation operations (unified event-driven flow)\n 'generation:start': {\n annotationUri: string;\n resourceUri: string;\n options: {\n title: string;\n prompt?: string;\n language?: string;\n temperature?: number;\n maxTokens?: number;\n context: any; // GenerationContext - required for generation\n };\n };\n 'generation:progress': any; // GenerationProgress from SSE\n 'generation:complete': { annotationUri: string; progress: any };\n 'generation:failed': { error: Error };\n 'generation:modal-open': {\n annotationUri: string;\n resourceUri: string;\n defaultTitle: string;\n };\n 'reference:create-manual': {\n annotationUri: string;\n title: string;\n entityTypes: string[];\n };\n 'reference:link': {\n annotationUri: string;\n searchTerm: string;\n };\n 'reference:search-modal-open': {\n referenceId: string;\n searchTerm: string;\n };\n};\n\nexport type EventBus = ReturnType<typeof mitt<EventMap>> & { busId: string };\n\nconst EventBusContext = createContext<EventBus | null>(null);\n\n/**\n * Generate an 8-digit hex identifier for an event bus instance\n */\nfunction generateBusId(): string {\n return Math.floor(Math.random() * 0xFFFFFFFF).toString(16).padStart(8, '0');\n}\n\n/**\n * Create an EventBus instance with logging and unique identifier\n */\nfunction createEventBus(): EventBus {\n const bus = mitt<EventMap>() as EventBus;\n const busId = generateBusId();\n\n // Add busId property\n bus.busId = busId;\n\n // Wrap emit to add logging with busId\n const originalEmit = bus.emit.bind(bus);\n bus.emit = ((eventName: any, payload?: any) => {\n console.info(`[EventBus:${busId}] emit:`, eventName, payload);\n return originalEmit(eventName, payload);\n }) as any;\n\n // Wrap on to add logging with busId\n const originalOn = bus.on.bind(bus);\n bus.on = ((eventName: any, handler: any) => {\n console.debug(`[EventBus:${busId}] subscribe:`, eventName);\n return originalOn(eventName, handler);\n }) as any;\n\n // Wrap off to add logging with busId\n const originalOff = bus.off.bind(bus);\n bus.off = ((eventName: any, handler?: any) => {\n console.debug(`[EventBus:${busId}] unsubscribe:`, eventName);\n return originalOff(eventName, handler);\n }) as any;\n\n return bus;\n}\n\n/**\n * Global singleton event bus.\n *\n * This ensures all components in the application share the same event bus instance,\n * which is critical for cross-component communication (e.g., hovering an annotation\n * in one component scrolls the panel in another component).\n *\n * FUTURE: Multi-Window Support\n * When we need to support multiple document windows (e.g., pop-out resource viewers),\n * we'll need to transition to a per-window event bus architecture:\n *\n * Option 1: Window-scoped event bus\n * - Create a new event bus for each window/portal\n * - Pass windowId or documentId to EventBusProvider\n * - Store Map<windowId, EventBus> instead of single global\n * - Components use useEventBus(windowId) to get correct bus\n *\n * Option 2: Event bus hierarchy\n * - Global event bus for app-wide events (settings, navigation)\n * - Per-document event bus for document-specific events (annotation hover)\n * - Components subscribe to both buses as needed\n *\n * Option 3: Cross-window event bridge\n * - Keep per-window buses isolated\n * - Use BroadcastChannel or postMessage for cross-window events\n * - Bridge pattern to sync certain events across windows\n *\n * For now, single global bus is correct for single-window app.\n */\nlet globalEventBus = createEventBus();\n\n/**\n * Reset the global event bus - FOR TESTING ONLY.\n *\n * Call this in test setup (beforeEach) to ensure test isolation.\n * Each test gets a fresh event bus with no lingering subscriptions.\n *\n * @example\n * ```typescript\n * beforeEach(() => {\n * resetEventBusForTesting();\n * });\n * ```\n */\nexport function resetEventBusForTesting() {\n globalEventBus = createEventBus();\n}\n\nexport interface EventBusProviderProps {\n children: ReactNode;\n // rUri and client removed - operation handlers are now set up via useEventOperations hook\n}\n\n/**\n * Unified event bus provider for all application events\n *\n * Consolidates three previous event buses:\n * - MakeMeaningEventBus (document/annotation operations)\n * - NavigationEventBus (navigation and sidebar UI)\n * - GlobalSettingsEventBus (app-wide settings)\n *\n * Benefits:\n * - Single import: useEventBus()\n * - No decision fatigue about which bus to use\n * - Easier cross-domain coordination\n * - Simpler provider hierarchy\n *\n * NOTE: This provider uses a global singleton event bus to ensure all components\n * share the same instance. Multiple providers in the tree will all reference the\n * same global bus.\n *\n * Operation handlers (API calls triggered by events) are set up separately via\n * the useEventOperations hook, which should be called at the resource page level.\n */\nexport function EventBusProvider({\n children,\n}: EventBusProviderProps) {\n const eventBus = useMemo(() => globalEventBus, []);\n\n return (\n <EventBusContext.Provider value={eventBus}>\n {children}\n </EventBusContext.Provider>\n );\n}\n\n/**\n * Hook to access the unified event bus\n *\n * Use this everywhere instead of:\n * - useMakeMeaningEvents()\n * - useNavigationEvents()\n * - useGlobalSettingsEvents()\n *\n * @example\n * ```typescript\n * const eventBus = useEventBus();\n *\n * // Emit any event\n * eventBus.emit('annotation:hover', { annotationId: '123' });\n * eventBus.emit('navigation:sidebar-toggle', undefined);\n * eventBus.emit('settings:theme-changed', { theme: 'dark' });\n *\n * // Subscribe to any event\n * useEffect(() => {\n * const handler = ({ annotationId }) => console.log(annotationId);\n * eventBus.on('annotation:hover', handler);\n * return () => eventBus.off('annotation:hover', handler);\n * }, []);\n * ```\n */\nexport function useEventBus(): EventBus {\n const bus = useContext(EventBusContext);\n if (!bus) {\n throw new Error('useEventBus must be used within EventBusProvider');\n }\n return bus;\n}\n","'use client';\n\nimport { createContext, useContext, ReactNode } from 'react';\nimport type { SessionManager } from '../types/SessionManager';\n\nconst SessionContext = createContext<SessionManager | null>(null);\n\n/**\n * Provider Pattern: Accepts SessionManager implementation as prop\n * and makes it available to child components via Context.\n *\n * Apps provide their own implementation (next-auth, custom auth, etc.)\n * and pass it to this provider at the root level.\n *\n * @example\n * ```tsx\n * // In app root\n * const sessionManager = useSessionManager(); // App's implementation\n *\n * <SessionProvider sessionManager={sessionManager}>\n * <App />\n * </SessionProvider>\n * ```\n */\nexport function SessionProvider({\n sessionManager,\n children\n}: {\n sessionManager: SessionManager;\n children: ReactNode;\n}) {\n return (\n <SessionContext.Provider value={sessionManager}>\n {children}\n </SessionContext.Provider>\n );\n}\n\n/**\n * Hook to access SessionManager from Context\n * Components use this hook to access session state and expiry information\n */\nexport function useSessionContext() {\n const context = useContext(SessionContext);\n if (!context) {\n throw new Error('useSessionContext must be used within SessionProvider');\n }\n return context;\n}","'use client';\n\nimport React, { useEffect, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport './Toast.css';\n\nexport type ToastType = 'success' | 'error' | 'info' | 'warning';\n\nexport interface ToastMessage {\n id: string;\n message: string;\n type: ToastType;\n duration?: number;\n}\n\ninterface ToastProps {\n toast: ToastMessage;\n onClose: (id: string) => void;\n}\n\nconst icons = {\n success: (\n <svg className=\"semiont-toast-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n ),\n error: (\n <svg className=\"semiont-toast-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n ),\n warning: (\n <svg className=\"semiont-toast-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\" />\n </svg>\n ),\n info: (\n <svg className=\"semiont-toast-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n ),\n};\n\nfunction Toast({ toast, onClose }: ToastProps) {\n useEffect(() => {\n const timer = setTimeout(() => {\n onClose(toast.id);\n }, toast.duration || 3000);\n\n return () => clearTimeout(timer);\n }, [toast, onClose]);\n\n return (\n <div\n className=\"semiont-toast\"\n data-variant={toast.type}\n role=\"alert\"\n >\n <div className=\"semiont-toast-icon-wrapper\">{icons[toast.type]}</div>\n <p className=\"semiont-toast-message\">{toast.message}</p>\n <button\n onClick={() => onClose(toast.id)}\n className=\"semiont-toast-close\"\n aria-label=\"Close\"\n >\n <svg className=\"semiont-toast-close-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n );\n}\n\ninterface ToastContainerProps {\n toasts: ToastMessage[];\n onClose: (id: string) => void;\n}\n\nexport function ToastContainer({ toasts, onClose }: ToastContainerProps) {\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n setMounted(true);\n return () => setMounted(false);\n }, []);\n\n if (!mounted) return null;\n\n return createPortal(\n <div className=\"semiont-toast-container\">\n {toasts.map((toast) => (\n <Toast key={toast.id} toast={toast} onClose={onClose} />\n ))}\n </div>,\n document.body\n );\n}\n\n// Toast context and hook for global toast management\ninterface ToastContextType {\n showToast: (message: string, type?: ToastType, duration?: number) => void;\n showSuccess: (message: string, duration?: number) => void;\n showError: (message: string, duration?: number) => void;\n showWarning: (message: string, duration?: number) => void;\n showInfo: (message: string, duration?: number) => void;\n}\n\nconst ToastContext = React.createContext<ToastContextType | undefined>(undefined);\n\nexport function ToastProvider({ children }: { children: React.ReactNode }) {\n const [toasts, setToasts] = useState<ToastMessage[]>([]);\n\n const showToast = React.useCallback((message: string, type: ToastType = 'info', duration?: number) => {\n const id = Date.now().toString();\n const newToast: ToastMessage = duration !== undefined\n ? { id, message, type, duration }\n : { id, message, type };\n setToasts((prev) => [...prev, newToast]);\n }, []);\n\n const showSuccess = React.useCallback((message: string, duration?: number) => showToast(message, 'success', duration), [showToast]);\n const showError = React.useCallback((message: string, duration?: number) => showToast(message, 'error', duration), [showToast]);\n const showWarning = React.useCallback((message: string, duration?: number) => showToast(message, 'warning', duration), [showToast]);\n const showInfo = React.useCallback((message: string, duration?: number) => showToast(message, 'info', duration), [showToast]);\n\n const handleClose = React.useCallback((id: string) => {\n setToasts((prev) => prev.filter((toast) => toast.id !== id));\n }, []);\n\n const contextValue = React.useMemo(\n () => ({ showToast, showSuccess, showError, showWarning, showInfo }),\n [showToast, showSuccess, showError, showWarning, showInfo]\n );\n\n return (\n <ToastContext.Provider value={contextValue}>\n {children}\n <ToastContainer toasts={toasts} onClose={handleClose} />\n </ToastContext.Provider>\n );\n}\n\nexport function useToast() {\n const context = React.useContext(ToastContext);\n if (context === undefined) {\n throw new Error('useToast must be used within a ToastProvider');\n }\n return context;\n}","'use client';\n\nimport React, { createContext, useContext } from 'react';\nimport type { OpenResourcesManager } from '../types/OpenResourcesManager';\n\nconst OpenResourcesContext = createContext<OpenResourcesManager | undefined>(undefined);\n\n/**\n * Provider Pattern: Accepts OpenResourcesManager implementation as prop\n * and makes it available to child components via Context.\n *\n * Apps provide their own implementation (localStorage, sessionStorage, database, etc.)\n * and pass it to this provider at the root level.\n *\n * @example\n * ```tsx\n * // In app root\n * const openResourcesManager = useOpenResourcesManager(); // App's implementation\n *\n * <OpenResourcesProvider openResourcesManager={openResourcesManager}>\n * <App />\n * </OpenResourcesProvider>\n * ```\n */\nexport function OpenResourcesProvider({\n openResourcesManager,\n children\n}: {\n openResourcesManager: OpenResourcesManager;\n children: React.ReactNode;\n}) {\n return (\n <OpenResourcesContext.Provider value={openResourcesManager}>\n {children}\n </OpenResourcesContext.Provider>\n );\n}\n\n/**\n * Hook to access OpenResourcesManager from Context\n * Components use this hook to access open resources functionality\n */\nexport function useOpenResources(): OpenResourcesManager {\n const context = useContext(OpenResourcesContext);\n if (context === undefined) {\n throw new Error('useOpenResources must be used within an OpenResourcesProvider');\n }\n return context;\n}","'use client';\n\nimport { createContext, useContext, ReactNode, useState, useEffect, useMemo } from 'react';\nimport type { TranslationManager } from '../types/TranslationManager';\n\n// Static import for default English only - always needed as fallback\nimport enTranslations from '../../translations/en.json';\n\nconst TranslationContext = createContext<TranslationManager | null>(null);\n\n// Cache for dynamically loaded translations\nconst translationCache = new Map<string, any>();\n\n/**\n * Process ICU MessageFormat plural syntax\n * Supports: {count, plural, =0 {text} =1 {text} other {text}}\n */\nfunction processPluralFormat(text: string, params: Record<string, any>): string {\n // Match {paramName, plural, ...} with proper brace counting\n const pluralMatch = text.match(/\\{(\\w+),\\s*plural,\\s*/);\n if (!pluralMatch) {\n return text;\n }\n\n const paramName = pluralMatch[1];\n const count = params[paramName];\n if (count === undefined) {\n return text;\n }\n\n // Find the matching closing brace by counting\n let startPos = pluralMatch[0].length;\n let braceCount = 1; // We're inside the first {\n let endPos = startPos;\n\n for (let i = startPos; i < text.length; i++) {\n if (text[i] === '{') braceCount++;\n else if (text[i] === '}') {\n braceCount--;\n if (braceCount === 0) {\n endPos = i;\n break;\n }\n }\n }\n\n const pluralCases = text.substring(startPos, endPos);\n\n // Parse plural cases: =0 {text} =1 {text} other {text}\n const cases: Record<string, string> = {};\n const caseRegex = /(?:=(\\d+)|(\\w+))\\s*\\{([^}]+)\\}/g;\n let caseMatch;\n\n while ((caseMatch = caseRegex.exec(pluralCases)) !== null) {\n const [, exactNumber, keyword, textContent] = caseMatch;\n const key = exactNumber !== undefined ? `=${exactNumber}` : keyword;\n cases[key] = textContent;\n }\n\n // Select appropriate case\n const exactMatch = cases[`=${count}`];\n if (exactMatch !== undefined) {\n const result = exactMatch.replace(/#/g, String(count));\n return text.substring(0, pluralMatch.index!) + result + text.substring(endPos + 1);\n }\n\n const otherCase = cases['other'];\n if (otherCase !== undefined) {\n const result = otherCase.replace(/#/g, String(count));\n return text.substring(0, pluralMatch.index!) + result + text.substring(endPos + 1);\n }\n\n return text;\n}\n\n// List of available locales (can be extended without importing all files)\nexport const AVAILABLE_LOCALES = [\n 'ar', // Arabic\n 'bn', // Bengali\n 'cs', // Czech\n 'da', // Danish\n 'de', // German\n 'el', // Greek\n 'en', // English\n 'es', // Spanish\n 'fa', // Persian/Farsi\n 'fi', // Finnish\n 'fr', // French\n 'he', // Hebrew\n 'hi', // Hindi\n 'id', // Indonesian\n 'it', // Italian\n 'ja', // Japanese\n 'ko', // Korean\n 'ms', // Malay\n 'nl', // Dutch\n 'no', // Norwegian\n 'pl', // Polish\n 'pt', // Portuguese\n 'ro', // Romanian\n 'sv', // Swedish\n 'th', // Thai\n 'tr', // Turkish\n 'uk', // Ukrainian\n 'vi', // Vietnamese\n 'zh', // Chinese\n] as const;\nexport type AvailableLocale = typeof AVAILABLE_LOCALES[number];\n\n// Lazy load translations for a specific locale\nasync function loadTranslations(locale: string): Promise<any> {\n // Check cache first\n if (translationCache.has(locale)) {\n return translationCache.get(locale);\n }\n\n // English is already loaded statically\n if (locale === 'en') {\n translationCache.set('en', enTranslations);\n return enTranslations;\n }\n\n try {\n // Dynamic import for all other locales\n const translations = await import(`../../translations/${locale}.json`);\n const translationData = translations.default || translations;\n translationCache.set(locale, translationData);\n return translationData;\n } catch (error) {\n console.error(`Failed to load translations for locale: ${locale}`, error);\n // Fall back to English\n return enTranslations;\n }\n}\n\n// Default English translation manager (using static import)\nconst defaultTranslationManager: TranslationManager = {\n t: (namespace: string, key: string, params?: Record<string, any>) => {\n const translations = enTranslations as Record<string, Record<string, string>>;\n const translation = translations[namespace]?.[key];\n\n if (!translation) {\n console.warn(`Translation not found for ${namespace}.${key}`);\n return `${namespace}.${key}`;\n }\n\n // Handle parameter interpolation and plural format\n if (params && typeof translation === 'string') {\n let result = translation;\n // First process plural format\n result = processPluralFormat(result, params);\n // Then handle simple parameter interpolation\n Object.entries(params).forEach(([paramKey, paramValue]) => {\n result = result.replace(new RegExp(`\\\\{${paramKey}\\\\}`, 'g'), String(paramValue));\n });\n return result;\n }\n\n return translation;\n },\n};\n\nexport interface TranslationProviderProps {\n /**\n * Option 1: Provide a complete TranslationManager implementation\n */\n translationManager?: TranslationManager;\n\n /**\n * Option 2: Use built-in translations by specifying a locale\n * When adding new locales, just add the JSON file and update AVAILABLE_LOCALES\n */\n locale?: string;\n\n /**\n * Loading component to show while translations are being loaded\n * Only relevant when using dynamic locale loading\n */\n loadingComponent?: ReactNode;\n\n children: ReactNode;\n}\n\n/**\n * Provider for translation management with dynamic loading\n *\n * Three modes of operation:\n * 1. No provider: Components use default English strings\n * 2. With locale prop: Dynamically loads translations for that locale\n * 3. With translationManager: Use custom translation implementation\n */\nexport function TranslationProvider({\n translationManager,\n locale,\n loadingComponent = null,\n children,\n}: TranslationProviderProps) {\n const [loadedTranslations, setLoadedTranslations] = useState<any>(null);\n const [isLoading, setIsLoading] = useState(false);\n\n // Load translations when locale changes\n useEffect(() => {\n if (locale && !translationManager) {\n setIsLoading(true);\n loadTranslations(locale)\n .then(translations => {\n setLoadedTranslations(translations);\n setIsLoading(false);\n })\n .catch(error => {\n console.error('Failed to load translations:', error);\n setLoadedTranslations(enTranslations); // Fall back to English\n setIsLoading(false);\n });\n }\n }, [locale, translationManager]);\n\n // Create translation manager from loaded translations\n const localeManager = useMemo<TranslationManager | null>(() => {\n if (!loadedTranslations) return null;\n\n return {\n t: (namespace: string, key: string, params?: Record<string, any>) => {\n const translation = loadedTranslations[namespace]?.[key];\n\n if (!translation) {\n console.warn(`Translation not found for ${namespace}.${key} in locale ${locale}`);\n return `${namespace}.${key}`;\n }\n\n // Handle parameter interpolation and plural format\n if (params && typeof translation === 'string') {\n let result = translation;\n // First process plural format\n result = processPluralFormat(result, params);\n // Then handle simple parameter interpolation\n Object.entries(params).forEach(([paramKey, paramValue]) => {\n result = result.replace(new RegExp(`\\\\{${paramKey}\\\\}`, 'g'), String(paramValue));\n });\n return result;\n }\n\n return translation;\n },\n };\n }, [loadedTranslations, locale]);\n\n // If custom translation manager provided, use it\n if (translationManager) {\n return (\n <TranslationContext.Provider value={translationManager}>\n {children}\n </TranslationContext.Provider>\n );\n }\n\n // If locale provided and still loading, show loading component\n if (locale && isLoading) {\n return <>{loadingComponent}</>;\n }\n\n // If locale provided and translations loaded, use them\n if (locale && localeManager) {\n return (\n <TranslationContext.Provider value={localeManager}>\n {children}\n </TranslationContext.Provider>\n );\n }\n\n // Default: use English translations\n return (\n <TranslationContext.Provider value={defaultTranslationManager}>\n {children}\n </TranslationContext.Provider>\n );\n}\n\n/**\n * Hook to access translations within a namespace\n *\n * Works in three modes:\n * 1. Without provider: Returns default English translations\n * 2. With provider using locale: Returns dynamically loaded translations for that locale\n * 3. With custom provider: Uses the custom translation manager\n *\n * @param namespace - Translation namespace (e.g., 'Toolbar', 'ResourceViewer')\n * @returns Function to translate keys within the namespace\n */\nexport function useTranslations(namespace: string) {\n const context = useContext(TranslationContext);\n\n // If no context (no provider), use default English translations\n if (!context) {\n return (key: string, params?: Record<string, any>) => {\n const translations = enTranslations as Record<string, Record<string, string>>;\n const translation = translations[namespace]?.[key];\n\n if (!translation) {\n console.warn(`Translation not found for ${namespace}.${key}`);\n return `${namespace}.${key}`;\n }\n\n // Handle parameter interpolation and plural format\n if (params && typeof translation === 'string') {\n let result = translation;\n // First process plural format\n result = processPluralFormat(result, params);\n // Then handle simple parameter interpolation\n Object.entries(params).forEach(([paramKey, paramValue]) => {\n result = result.replace(new RegExp(`\\\\{${paramKey}\\\\}`, 'g'), String(paramValue));\n });\n return result;\n }\n\n return translation;\n };\n }\n\n // Return a function that translates keys within this namespace\n return (key: string, params?: Record<string, any>) => context.t(namespace, key, params);\n}\n\n/**\n * Hook to preload translations for a locale\n * Useful for preloading translations before navigation\n */\nexport function usePreloadTranslations() {\n return {\n preload: async (locale: string) => {\n try {\n await loadTranslations(locale);\n return true;\n } catch (error) {\n console.error(`Failed to preload translations for ${locale}:`, error);\n return false;\n }\n },\n isLoaded: (locale: string) => translationCache.has(locale),\n };\n}"],"mappings":"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","names":["createContext","useContext","useMemo","jsx","createContext","useContext","jsx","jsx","createContext","useContext","jsx","createContext","useContext","useState","useEffect","useMemo","jsx","createContext","useState","useEffect","useMemo","jsx","useContext"]}
|