@semiont/react-ui 0.4.13 → 0.4.15
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 +18 -12
- package/dist/KnowledgeBaseSessionContext-CpYaCbnC.d.mts +174 -0
- package/dist/{PdfAnnotationCanvas.client-CW6SKH2U.mjs → PdfAnnotationCanvas.client-CHDCGQBR.mjs} +3 -3
- package/dist/{chunk-HNZOXH4L.mjs → chunk-OZICDVH7.mjs} +5 -3
- package/dist/chunk-OZICDVH7.mjs.map +1 -0
- package/dist/chunk-R2U7P4TK.mjs +865 -0
- package/dist/chunk-R2U7P4TK.mjs.map +1 -0
- package/dist/{chunk-BQJWOK4C.mjs → chunk-VN5NY4SN.mjs} +9 -8
- package/dist/chunk-VN5NY4SN.mjs.map +1 -0
- package/dist/index.d.mts +147 -171
- package/dist/index.mjs +2215 -1961
- package/dist/index.mjs.map +1 -1
- package/dist/test-utils.d.mts +13 -62
- package/dist/test-utils.mjs +40 -21
- package/dist/test-utils.mjs.map +1 -1
- package/package.json +5 -3
- package/src/components/ProtectedErrorBoundary.tsx +95 -0
- package/src/components/Toolbar.tsx +13 -13
- package/src/components/__tests__/ProtectedErrorBoundary.test.tsx +197 -0
- package/src/components/modals/PermissionDeniedModal.tsx +140 -0
- package/src/components/modals/ReferenceWizardModal.tsx +3 -2
- package/src/components/modals/SessionExpiredModal.tsx +101 -0
- package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +150 -0
- package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +115 -0
- package/src/components/resource/AnnotationHistory.tsx +5 -6
- package/src/components/resource/HistoryEvent.tsx +7 -7
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +33 -34
- package/src/components/resource/__tests__/HistoryEvent.test.tsx +17 -19
- package/src/components/resource/__tests__/event-formatting.test.ts +70 -94
- package/src/components/resource/event-formatting.ts +56 -56
- package/src/components/resource/panels/CollaborationPanel.tsx +9 -1
- package/src/components/resource/panels/ReferenceEntry.tsx +7 -5
- package/src/components/resource/panels/ResourceInfoPanel.tsx +8 -6
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +12 -12
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +1 -0
- package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +1 -1
- package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +4 -4
- package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +5 -10
- package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +23 -54
- package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +6 -6
- package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +7 -19
- package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +1 -1
- package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +18 -44
- package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +6 -6
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +31 -26
- package/src/styles/patterns/panels-base.css +12 -0
- package/dist/TranslationManager-CudgH3gw.d.mts +0 -107
- package/dist/chunk-BQJWOK4C.mjs.map +0 -1
- package/dist/chunk-HNZOXH4L.mjs.map +0 -1
- package/dist/chunk-OL5UST25.mjs +0 -413
- package/dist/chunk-OL5UST25.mjs.map +0 -1
- /package/dist/{PdfAnnotationCanvas.client-CW6SKH2U.mjs.map → PdfAnnotationCanvas.client-CHDCGQBR.mjs.map} +0 -0
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
9
9
|
import { useQueryClient } from '@tanstack/react-query';
|
|
10
|
-
import type { components, ResourceId,
|
|
10
|
+
import type { components, ResourceId, GatheredContext, EventMap } from '@semiont/core';
|
|
11
11
|
import { annotationId } from '@semiont/core';
|
|
12
12
|
import { getLanguage, getPrimaryRepresentation, getPrimaryMediaType, getMimeCategory } from '@semiont/api-client';
|
|
13
13
|
import { ANNOTATORS } from '@semiont/react-ui';
|
|
@@ -90,6 +90,11 @@ export interface ResourceViewerPageProps {
|
|
|
90
90
|
* SSE attention stream connection status for the active workspace
|
|
91
91
|
*/
|
|
92
92
|
streamStatus: StreamStatus;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Name of the active knowledge base (for display in panels)
|
|
96
|
+
*/
|
|
97
|
+
knowledgeBaseName?: string | undefined;
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
/**
|
|
@@ -128,13 +133,14 @@ export function ResourceViewerPage({
|
|
|
128
133
|
ToolbarPanels,
|
|
129
134
|
refetchDocument,
|
|
130
135
|
streamStatus,
|
|
136
|
+
knowledgeBaseName,
|
|
131
137
|
}: ResourceViewerPageProps) {
|
|
132
138
|
// Translations
|
|
133
139
|
const tw = useTranslations('ReferenceWizard');
|
|
134
140
|
|
|
135
141
|
// Get unified event bus for subscribing to UI events
|
|
136
142
|
const eventBus = useEventBus();
|
|
137
|
-
const
|
|
143
|
+
const semiont = useApiClient();
|
|
138
144
|
const queryClient = useQueryClient(); // retained for non-store queries (events log)
|
|
139
145
|
|
|
140
146
|
// UI state hooks
|
|
@@ -158,17 +164,17 @@ export function ResourceViewerPage({
|
|
|
158
164
|
|
|
159
165
|
// Binary path: fetch short-lived media token, construct URL
|
|
160
166
|
const { token: mediaToken, loading: mediaTokenLoading } = useMediaToken(rUri);
|
|
161
|
-
const binaryContent = (isBinary && mediaToken &&
|
|
162
|
-
? `${
|
|
167
|
+
const binaryContent = (isBinary && mediaToken && semiont)
|
|
168
|
+
? `${semiont.baseUrl}/api/resources/${rUri}?token=${mediaToken}`
|
|
163
169
|
: '';
|
|
164
170
|
|
|
165
171
|
const content = isBinary ? binaryContent : textContent;
|
|
166
172
|
const contentLoading = isBinary ? mediaTokenLoading : textLoading;
|
|
167
173
|
|
|
168
|
-
const annotationsData = useObservable(
|
|
174
|
+
const annotationsData = useObservable(semiont.browse.annotations(rUri));
|
|
169
175
|
const annotations = useMemo(
|
|
170
|
-
() => annotationsData
|
|
171
|
-
[annotationsData
|
|
176
|
+
() => annotationsData || [],
|
|
177
|
+
[annotationsData]
|
|
172
178
|
);
|
|
173
179
|
|
|
174
180
|
const { data: referencedByData, isLoading: referencedByLoading } = resources.referencedBy.useQuery(rUri);
|
|
@@ -203,8 +209,8 @@ export function ResourceViewerPage({
|
|
|
203
209
|
setWizardEntityTypes(event.entityTypes);
|
|
204
210
|
setWizardOpen(true);
|
|
205
211
|
|
|
206
|
-
// Trigger context gathering
|
|
207
|
-
eventBus.get('gather:requested').next({ correlationId: crypto.randomUUID(), annotationId: event.annotationId, resourceId: event.resourceId });
|
|
212
|
+
// Trigger context gathering — gather:requested is consumed by useContextGatherFlow
|
|
213
|
+
eventBus.get('gather:requested').next({ correlationId: crypto.randomUUID(), annotationId: event.annotationId, resourceId: event.resourceId, options: { contextWindow: 2000 } });
|
|
208
214
|
});
|
|
209
215
|
return () => subscription.unsubscribe();
|
|
210
216
|
}, [eventBus]);
|
|
@@ -225,21 +231,18 @@ export function ResourceViewerPage({
|
|
|
225
231
|
});
|
|
226
232
|
}, [onGenerateDocument]);
|
|
227
233
|
|
|
228
|
-
const handleWizardLinkResource = useCallback((referenceId: string, targetResourceId: string) => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
op: 'add',
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
});
|
|
241
|
-
showSuccess('Reference linked successfully');
|
|
242
|
-
}, [rUri, showSuccess]); // eventBus is stable singleton
|
|
234
|
+
const handleWizardLinkResource = useCallback(async (referenceId: string, targetResourceId: string) => {
|
|
235
|
+
try {
|
|
236
|
+
await semiont.bind.body(
|
|
237
|
+
rUri,
|
|
238
|
+
annotationId(referenceId),
|
|
239
|
+
[{ op: 'add', item: { type: 'SpecificResource' as const, source: targetResourceId, purpose: 'linking' as const } }],
|
|
240
|
+
);
|
|
241
|
+
showSuccess('Reference linked successfully');
|
|
242
|
+
} catch (error) {
|
|
243
|
+
showError(`Failed to link reference: ${error instanceof Error ? error.message : String(error)}`);
|
|
244
|
+
}
|
|
245
|
+
}, [rUri, semiont, showSuccess, showError]);
|
|
243
246
|
|
|
244
247
|
const handleWizardComposeNavigate = useCallback((
|
|
245
248
|
context: GatheredContext,
|
|
@@ -358,8 +361,8 @@ export function ResourceViewerPage({
|
|
|
358
361
|
triggerSparkleAnimation(annotationId);
|
|
359
362
|
}, [triggerSparkleAnimation]);
|
|
360
363
|
|
|
361
|
-
const handleAnnotationAdded = useCallback((
|
|
362
|
-
triggerSparkleAnimation(
|
|
364
|
+
const handleAnnotationAdded = useCallback((stored: EventMap['mark:added']) => {
|
|
365
|
+
triggerSparkleAnimation(stored.payload.annotation.id);
|
|
363
366
|
}, [triggerSparkleAnimation]);
|
|
364
367
|
|
|
365
368
|
const handleAnnotationCreateFailed = useCallback(() => showError('Failed to create annotation'), [showError]);
|
|
@@ -609,6 +612,7 @@ export function ResourceViewerPage({
|
|
|
609
612
|
{/* Document Info Panel */}
|
|
610
613
|
{activePanel === 'info' && (
|
|
611
614
|
<ResourceInfoPanel
|
|
615
|
+
resourceId={rUri}
|
|
612
616
|
documentEntityTypes={documentEntityTypes}
|
|
613
617
|
documentLocale={getLanguage(resource)}
|
|
614
618
|
primaryMediaType={primaryMediaType}
|
|
@@ -628,6 +632,7 @@ export function ResourceViewerPage({
|
|
|
628
632
|
<CollaborationPanel
|
|
629
633
|
isConnected={streamStatus === 'connected'}
|
|
630
634
|
eventCount={0}
|
|
635
|
+
knowledgeBaseName={knowledgeBaseName}
|
|
631
636
|
/>
|
|
632
637
|
)}
|
|
633
638
|
|
|
@@ -401,4 +401,16 @@
|
|
|
401
401
|
|
|
402
402
|
.semiont-panel-actions--between {
|
|
403
403
|
justify-content: space-between;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* Login form pulsing border for first-launch nudge */
|
|
407
|
+
@keyframes semiont-pulse-border {
|
|
408
|
+
0%, 100% { border-color: var(--semiont-color-primary-300, #93c5fd); }
|
|
409
|
+
50% { border-color: var(--semiont-color-primary-600, #2563eb); }
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.semiont-panel__login-form--pulsing {
|
|
413
|
+
border: 2px solid var(--semiont-color-primary-400, #60a5fa);
|
|
414
|
+
border-radius: var(--semiont-panel-border-radius, 0.5rem);
|
|
415
|
+
animation: semiont-pulse-border 2s ease-in-out infinite;
|
|
404
416
|
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Open Resources Manager Interface
|
|
3
|
-
*
|
|
4
|
-
* Manages a list of open resources (documents/files) with persistence.
|
|
5
|
-
* This interface allows apps to provide their own implementation of resource management
|
|
6
|
-
* (localStorage, sessionStorage, database, etc.) while components remain framework-agnostic.
|
|
7
|
-
*
|
|
8
|
-
* Components accept this manager as a prop instead of consuming from Context.
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```tsx
|
|
12
|
-
* // In app (e.g., frontend/src/hooks/useOpenResourcesManager.ts)
|
|
13
|
-
* export function useOpenResourcesManager(): OpenResourcesManager {
|
|
14
|
-
* const [openResources, setOpenResources] = useState<OpenResource[]>([]);
|
|
15
|
-
*
|
|
16
|
-
* // Implementation details...
|
|
17
|
-
*
|
|
18
|
-
* return {
|
|
19
|
-
* openResources,
|
|
20
|
-
* addResource,
|
|
21
|
-
* removeResource,
|
|
22
|
-
* updateResourceName,
|
|
23
|
-
* reorderResources
|
|
24
|
-
* };
|
|
25
|
-
* }
|
|
26
|
-
*
|
|
27
|
-
* // Pass to components as props
|
|
28
|
-
* <KnowledgeNavigation openResourcesManager={openResourcesManager} />
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
interface OpenResource {
|
|
32
|
-
/** Unique identifier for the resource */
|
|
33
|
-
id: string;
|
|
34
|
-
/** Display name of the resource */
|
|
35
|
-
name: string;
|
|
36
|
-
/** Timestamp when the resource was opened */
|
|
37
|
-
openedAt: number;
|
|
38
|
-
/** Order/position for manual sorting (optional for backward compatibility) */
|
|
39
|
-
order?: number;
|
|
40
|
-
/** Media type for icon display (e.g., 'application/pdf', 'text/plain') */
|
|
41
|
-
mediaType?: string;
|
|
42
|
-
/** Working-tree URI (e.g. "file://docs/overview.md") — used as tooltip in navigation */
|
|
43
|
-
storageUri?: string;
|
|
44
|
-
}
|
|
45
|
-
interface OpenResourcesManager {
|
|
46
|
-
/** List of currently open resources */
|
|
47
|
-
openResources: OpenResource[];
|
|
48
|
-
/**
|
|
49
|
-
* Add a new resource to the open list or update if already exists
|
|
50
|
-
* @param id - Unique resource identifier
|
|
51
|
-
* @param name - Display name of the resource
|
|
52
|
-
* @param mediaType - Optional media type for icon display
|
|
53
|
-
* @param storageUri - Optional working-tree URI (e.g. "file://docs/overview.md")
|
|
54
|
-
*/
|
|
55
|
-
addResource: (id: string, name: string, mediaType?: string, storageUri?: string) => void;
|
|
56
|
-
/**
|
|
57
|
-
* Remove a resource from the open list
|
|
58
|
-
* @param id - Resource identifier to remove
|
|
59
|
-
*/
|
|
60
|
-
removeResource: (id: string) => void;
|
|
61
|
-
/**
|
|
62
|
-
* Update the display name of an open resource
|
|
63
|
-
* @param id - Resource identifier
|
|
64
|
-
* @param name - New display name
|
|
65
|
-
*/
|
|
66
|
-
updateResourceName: (id: string, name: string) => void;
|
|
67
|
-
/**
|
|
68
|
-
* Reorder resources by moving from one index to another
|
|
69
|
-
* @param oldIndex - Current position index
|
|
70
|
-
* @param newIndex - Desired position index
|
|
71
|
-
*/
|
|
72
|
-
reorderResources: (oldIndex: number, newIndex: number) => void;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Session management interface for handling authentication state and session expiry
|
|
77
|
-
* Apps implement this interface and pass it to SessionProvider
|
|
78
|
-
*/
|
|
79
|
-
interface SessionState {
|
|
80
|
-
/** Whether the user is currently authenticated */
|
|
81
|
-
isAuthenticated: boolean;
|
|
82
|
-
/** When the session expires (null if not authenticated) */
|
|
83
|
-
expiresAt: Date | null;
|
|
84
|
-
/** Time in milliseconds until session expires (null if not authenticated) */
|
|
85
|
-
timeUntilExpiry: number | null;
|
|
86
|
-
/** Whether the session is expiring soon (< 5 minutes) */
|
|
87
|
-
isExpiringSoon: boolean;
|
|
88
|
-
}
|
|
89
|
-
interface SessionManager extends SessionState {
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Translation management interface
|
|
94
|
-
* Apps implement this to provide translations using their preferred i18n library
|
|
95
|
-
*/
|
|
96
|
-
interface TranslationManager {
|
|
97
|
-
/**
|
|
98
|
-
* Translate a key within a namespace
|
|
99
|
-
* @param namespace - Translation namespace (e.g., 'Toolbar', 'ResourceViewer')
|
|
100
|
-
* @param key - Translation key within the namespace
|
|
101
|
-
* @param params - Optional parameters for interpolation
|
|
102
|
-
* @returns Translated string
|
|
103
|
-
*/
|
|
104
|
-
t: (namespace: string, key: string, params?: Record<string, any>) => string;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export type { OpenResourcesManager as O, SessionManager as S, TranslationManager as T, OpenResource as a, SessionState as b };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/contexts/AuthTokenContext.tsx","../src/contexts/useEventSubscription.ts","../src/hooks/useBeckonFlow.ts"],"sourcesContent":["'use client';\n\n/**\n * Auth Token Context - Manages authentication token lifecycle\n *\n * Simple approach: Just pass the token value through context.\n * When the token changes, context updates, components re-render.\n * No complex machinery needed.\n */\n\nimport { createContext, useContext, ReactNode } from 'react';\n\nconst AuthTokenContext = createContext<string | null | undefined>(undefined);\n\nexport interface AuthTokenProviderProps {\n token: string | null;\n children: ReactNode;\n}\n\n/**\n * Provider for auth token\n * Pass the current token value - React handles the rest\n */\nexport function AuthTokenProvider({\n token,\n children,\n}: AuthTokenProviderProps) {\n return (\n <AuthTokenContext.Provider value={token}>\n {children}\n </AuthTokenContext.Provider>\n );\n}\n\n/**\n * Hook to get current auth token\n *\n * Returns the current token value from context.\n * Re-renders automatically when token changes (normal React behavior).\n *\n * @returns Current access token (null if not authenticated)\n * @throws Error if used outside AuthTokenProvider\n */\nexport function useAuthToken(): string | null {\n const context = useContext(AuthTokenContext);\n\n if (context === undefined) {\n throw new Error('useAuthToken must be used within an AuthTokenProvider');\n }\n\n return context;\n}\n","import { useEffect, useRef, useMemo } from 'react';\nimport type { EventMap } from '@semiont/core';\nimport { useEventBus } from './EventBusContext';\n\n/**\n * Subscribe to an event bus event with automatic cleanup.\n *\n * This hook solves the \"stale closure\" problem by always using the latest\n * version of the handler without re-subscribing.\n *\n * @example\n * ```tsx\n * useEventSubscription('mark:created', ({ annotation }) => {\n * // This always uses the latest props/state\n * triggerSparkleAnimation(annotation.id);\n * });\n * ```\n */\nexport function useEventSubscription<K extends keyof EventMap>(\n eventName: K,\n handler: (payload: EventMap[K]) => void\n): void {\n const eventBus = useEventBus();\n\n // Store the latest handler in a ref to avoid stale closures\n const handlerRef = useRef(handler);\n\n // Update ref on every render (no re-subscription needed)\n useEffect(() => {\n handlerRef.current = handler;\n });\n\n // Subscribe once, using a stable wrapper that calls the current handler\n useEffect(() => {\n const stableHandler = (payload: EventMap[K]) => {\n handlerRef.current(payload);\n };\n\n // RxJS EventBus.get() returns Subject, subscribe returns Subscription\n const subscription = eventBus.get(eventName).subscribe(stableHandler);\n\n return () => {\n subscription.unsubscribe();\n };\n }, [eventName, eventBus]); // eventBus is stable, only re-subscribe if event name changes\n}\n\n/**\n * Subscribe to multiple events at once.\n *\n * @example\n * ```tsx\n * useEventSubscriptions({\n * 'mark:created': ({ annotation }) => setNewAnnotation(annotation),\n * 'mark:deleted': ({ annotationId }) => removeAnnotation(annotationId),\n * });\n * ```\n */\nexport function useEventSubscriptions(\n subscriptions: {\n [K in keyof EventMap]?: (payload: EventMap[K]) => void;\n }\n): void {\n const eventBus = useEventBus();\n\n // Store the latest handlers in refs\n const handlersRef = useRef(subscriptions);\n\n // Update refs on every render\n useEffect(() => {\n handlersRef.current = subscriptions;\n });\n\n // Get stable list of event names to subscribe to\n const eventNames = useMemo(\n () => Object.keys(subscriptions).sort(),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [Object.keys(subscriptions).sort().join(',')]\n );\n\n // Subscribe once per event - only re-subscribe if event names actually change\n useEffect(() => {\n const subscriptions: Array<{ unsubscribe: () => void }> = [];\n\n // Create stable wrappers for each subscription\n for (const eventName of eventNames) {\n const stableHandler = (payload: any) => {\n const currentHandler = handlersRef.current[eventName as keyof EventMap];\n if (currentHandler) {\n currentHandler(payload);\n } else {\n console.warn('[useEventSubscriptions] No current handler found for:', eventName);\n }\n };\n\n // RxJS EventBus.get() returns Subject, subscribe returns Subscription\n const subscription = eventBus.get(eventName as keyof EventMap).subscribe(stableHandler);\n subscriptions.push(subscription);\n }\n\n // Cleanup: unsubscribe from all subscriptions\n return () => {\n for (const subscription of subscriptions) {\n subscription.unsubscribe();\n }\n };\n }, [eventNames, eventBus]); // eventBus is stable singleton - never in deps; only re-subscribe if event names change\n}\n","/**\n * useBeckonFlow — Annotation attention / pointer coordination hook\n *\n * Manages which annotation currently has the user's attention:\n * - Hover state (hoveredAnnotationId)\n * - Hover → sparkle relay\n * - Click → focus relay\n *\n * Follows react-rxjs-guide.md Layer 2 pattern: Hook bridge that\n * subscribes to events and pushes values into React state.\n *\n * Note: beckon:sparkle visual effect (triggerSparkleAnimation) is owned by\n * ResourceViewerPage, which subscribes to beckon:sparkle and delegates to\n * ResourceAnnotationsContext. This hook emits the signal; it does not render the effect.\n *\n * @subscribes beckon:hover - Sets hoveredAnnotationId; emits beckon:sparkle\n * @subscribes browse:click - Emits beckon:focus (attention relay only)\n * @emits beckon:sparkle\n * @emits beckon:focus\n */\n\n/**\n * useHoverEmitter / createHoverHandlers — annotation hover emission utilities\n *\n * Centralises two hover quality-of-life behaviours:\n *\n * 1. currentHover guard — suppresses redundant emissions when the mouse\n * moves within the same annotation element (prevents event bus noise).\n *\n * 2. Debounce delay (HOVER_DELAY_MS) — a short timer before emitting\n * beckon:hover, so that transient pass-through movements (user dragging\n * the mouse across the panel to reach a button elsewhere) do not trigger\n * sparkle animations or cross-highlight effects.\n * The delay is cancelled immediately on mouseLeave, so leaving is always instant.\n *\n * Two forms are provided:\n *\n * useHoverEmitter(annotationId)\n * React hook. Returns { onMouseEnter, onMouseLeave } props for JSX elements.\n * Use in panel entries (HighlightEntry, CommentEntry, …).\n *\n * createHoverHandlers(emit)\n * Plain factory. Returns { handleMouseEnter(id), handleMouseLeave(), cleanup }.\n * Use inside useEffect / imperative setup code where hooks cannot be called\n * (BrowseView, CodeMirrorRenderer, AnnotationOverlay, PdfAnnotationCanvas).\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { accessToken } from '@semiont/core';\nimport { useEventBus } from '../contexts/EventBusContext';\nimport { useEventSubscriptions } from '../contexts/useEventSubscription';\nimport { useApiClient } from '../contexts/ApiClientContext';\nimport { useAuthToken } from '../contexts/AuthTokenContext';\nimport type { StreamStatus } from './useResourceEvents';\n\n// ─── useBeckonFlow ─────────────────────────────────────────────────────────\n\nexport interface BeckonFlowState {\n hoveredAnnotationId: string | null;\n}\n\nexport function useBeckonFlow(): BeckonFlowState {\n const eventBus = useEventBus();\n const [hoveredAnnotationId, setHoveredAnnotationId] = useState<string | null>(null);\n\n const handleAnnotationHover = useCallback(({ annotationId }: { annotationId: string | null }) => {\n setHoveredAnnotationId(annotationId);\n if (annotationId) {\n eventBus.get('beckon:sparkle').next({ annotationId });\n }\n }, []); // eventBus is stable singleton - never in deps\n\n const handleAnnotationClick = useCallback(({ annotationId }: { annotationId: string }) => {\n eventBus.get('beckon:focus').next({ annotationId });\n // Scroll to annotation handled by BrowseView via beckon:focus subscription\n }, []); // eventBus is stable singleton - never in deps\n\n useEventSubscriptions({\n 'beckon:hover': handleAnnotationHover,\n 'browse:click': handleAnnotationClick,\n });\n\n return { hoveredAnnotationId };\n}\n\n// ─── createHoverHandlers (use inside useEffect / imperative setup) ────────────\n\n/** Default milliseconds the mouse must dwell before beckon:hover is emitted. */\nexport const HOVER_DELAY_MS = 150;\n\ntype EmitHover = (annotationId: string | null) => void;\n\nexport interface HoverHandlers {\n /** Call with the annotation ID when the mouse enters an annotation element. */\n handleMouseEnter: (annotationId: string) => void;\n /** Call when the mouse leaves the annotation element. */\n handleMouseLeave: () => void;\n /** Cancel any pending timer — call in the useEffect cleanup. */\n cleanup: () => void;\n}\n\n/**\n * Creates hover handlers for imperative code (non-hook contexts).\n * @param emit - Callback to emit hover events\n * @param delayMs - Hover delay in milliseconds\n */\nexport function createHoverHandlers(emit: EmitHover, delayMs: number): HoverHandlers {\n let currentHover: string | null = null;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const cancelTimer = () => {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n };\n\n const handleMouseEnter = (annotationId: string) => {\n if (currentHover === annotationId) return; // already hovering this one\n cancelTimer();\n timer = setTimeout(() => {\n timer = null;\n currentHover = annotationId;\n emit(annotationId);\n }, delayMs);\n };\n\n const handleMouseLeave = () => {\n cancelTimer();\n if (currentHover !== null) {\n currentHover = null;\n emit(null);\n }\n };\n\n return { handleMouseEnter, handleMouseLeave, cleanup: cancelTimer };\n}\n\n// ─── useHoverEmitter (use in JSX onMouseEnter / onMouseLeave props) ───────────\n\nexport interface HoverEmitterProps {\n onMouseEnter: () => void;\n onMouseLeave: () => void;\n}\n\n/**\n * React hook that returns onMouseEnter / onMouseLeave props for a single\n * annotation entry element.\n *\n * @param annotationId - The ID of the annotation this element represents.\n * @param hoverDelayMs - Hover delay in milliseconds (defaults to HOVER_DELAY_MS for panel entries)\n */\nexport function useHoverEmitter(annotationId: string, hoverDelayMs: number = HOVER_DELAY_MS): HoverEmitterProps {\n const eventBus = useEventBus();\n const currentHoverRef = useRef<string | null>(null);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const onMouseEnter = useCallback(() => {\n if (currentHoverRef.current === annotationId) return;\n if (timerRef.current !== null) {\n clearTimeout(timerRef.current);\n }\n timerRef.current = setTimeout(() => {\n timerRef.current = null;\n currentHoverRef.current = annotationId;\n eventBus.get('beckon:hover').next({ annotationId });\n }, hoverDelayMs);\n }, [annotationId, hoverDelayMs]); // eventBus is stable singleton - never in deps\n\n const onMouseLeave = useCallback(() => {\n if (timerRef.current !== null) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n if (currentHoverRef.current !== null) {\n currentHoverRef.current = null;\n eventBus.get('beckon:hover').next({ annotationId: null });\n }\n }, []); // eventBus is stable singleton - never in deps\n\n // Cleanup timer on unmount\n useEffect(() => {\n return () => {\n if (timerRef.current !== null) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n };\n }, []);\n\n return { onMouseEnter, onMouseLeave };\n}\n\n// ─── useAttentionStream ───────────────────────────────────────────────────────\n\n/**\n * Opens a participant-scoped SSE connection to receive cross-participant\n * beckon signals. Each incoming signal is emitted as beckon:focus on the\n * EventBus — the existing scroll/highlight machinery handles it automatically.\n *\n * Signals are ephemeral: delivered if connected, silently dropped if not.\n *\n * @example\n * ```tsx\n * // In your layout (render-nothing connector):\n * useAttentionStream();\n * ```\n */\nexport function useAttentionStream(): { status: StreamStatus } {\n const client = useApiClient();\n const token = useAuthToken();\n const tokenRef = useRef(token);\n useEffect(() => { tokenRef.current = token; });\n const [status, setStatus] = useState<StreamStatus>('disconnected');\n\n useEffect(() => {\n setStatus('connecting');\n try {\n const sub = client.flows.attentionStream(() =>\n tokenRef.current ? accessToken(tokenRef.current) : undefined\n );\n setStatus('connected');\n return () => { sub.unsubscribe(); setStatus('disconnected'); };\n } catch (error) {\n console.error('[AttentionStream] Failed to connect:', error);\n setStatus('error');\n return;\n }\n }, [client]);\n\n return { status };\n}\n"],"mappings":"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","names":["subscriptions","useRef","useEffect","useRef","useEffect"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/contexts/EventBusContext.tsx","../src/contexts/ApiClientContext.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, useRef, type ReactNode } from 'react';\nimport { EventBus } from '@semiont/core';\n\nconst EventBusContext = createContext<EventBus | null>(null);\n\nexport interface EventBusProviderProps {\n children: ReactNode;\n}\n\n/**\n * Unified event bus provider for all application events.\n *\n * Each provider mount creates a fresh EventBus instance. This means:\n * - Workspace switches (which remount via key prop) get isolated buses\n * - Tests get isolation naturally — no resetEventBusForTesting needed\n *\n * Operation handlers (API calls triggered by events) are set up separately via\n * the useBindFlow hook, which should be called at the resource page level.\n */\nexport function EventBusProvider({ children }: EventBusProviderProps) {\n const eventBusRef = useRef<EventBus | null>(null);\n if (!eventBusRef.current) {\n eventBusRef.current = new EventBus();\n }\n const eventBus = eventBusRef.current;\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.get('beckon:hover').next({ annotationId: '123' });\n * eventBus.get('browse:sidebar-toggle').next(undefined);\n * eventBus.get('settings:theme-changed').next({ theme: 'dark' });\n *\n * // Subscribe to any event\n * useEffect(() => {\n * const unsubscribe = eventBus.on('beckon:hover', ({ annotationId }) => {\n * console.log(annotationId);\n * });\n * return () => unsubscribe();\n * }, []);\n * ```\n */\nexport function useEventBus(): EventBus {\n const eventBus = useContext(EventBusContext);\n if (!eventBus) {\n throw new Error('useEventBus must be used within EventBusProvider');\n }\n return eventBus;\n}\n","'use client';\n\nimport { createContext, useContext, ReactNode, useMemo } from 'react';\nimport { baseUrl } from '@semiont/core';\nimport { SemiontApiClient } from '@semiont/api-client';\nimport { useEventBus } from './EventBusContext';\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 — must be nested inside EventBusProvider.\n * The client is re-created when the baseUrl changes (workspace switch).\n * The EventBus is taken from EventBusContext so client and UI components\n * share the same workspace-scoped bus.\n */\nexport function ApiClientProvider({\n baseUrl: url,\n children,\n}: ApiClientProviderProps) {\n const eventBus = useEventBus();\n\n const client = useMemo(\n () => new SemiontApiClient({\n baseUrl: baseUrl(url),\n eventBus,\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, eventBus]\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"],"mappings":";;;AAEA,SAAS,eAAe,YAAY,cAA8B;AAClE,SAAS,gBAAgB;AA0BrB;AAxBJ,IAAM,kBAAkB,cAA+B,IAAI;AAgBpD,SAAS,iBAAiB,EAAE,SAAS,GAA0B;AACpE,QAAM,cAAc,OAAwB,IAAI;AAChD,MAAI,CAAC,YAAY,SAAS;AACxB,gBAAY,UAAU,IAAI,SAAS;AAAA,EACrC;AACA,QAAM,WAAW,YAAY;AAE7B,SACE,oBAAC,gBAAgB,UAAhB,EAAyB,OAAO,UAC9B,UACH;AAEJ;AA4BO,SAAS,cAAwB;AACtC,QAAM,WAAW,WAAW,eAAe;AAC3C,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AACA,SAAO;AACT;;;ACjEA,SAAS,iBAAAA,gBAAe,cAAAC,aAAuB,eAAe;AAC9D,SAAS,eAAe;AACxB,SAAS,wBAAwB;AAiC7B,gBAAAC,YAAA;AA9BJ,IAAM,mBAAmBC,eAA4C,MAAS;AAavE,SAAS,kBAAkB;AAAA,EAChC,SAAS;AAAA,EACT;AACF,GAA2B;AACzB,QAAM,WAAW,YAAY;AAE7B,QAAM,SAAS;AAAA,IACb,MAAM,IAAI,iBAAiB;AAAA,MACzB,SAAS,QAAQ,GAAG;AAAA,MACpB;AAAA;AAAA,MAEA,GAAI,QAAQ,IAAI,aAAa,UAAU,EAAE,SAAS,IAAM;AAAA,IAC1D,CAAC;AAAA,IACD,CAAC,KAAK,QAAQ;AAAA,EAChB;AAEA,SACE,gBAAAD,KAAC,iBAAiB,UAAjB,EAA0B,OAAO,QAC/B,UACH;AAEJ;AAOO,SAAS,eAAiC;AAC/C,QAAM,UAAUE,YAAW,gBAAgB;AAE3C,MAAI,YAAY,QAAW;AACzB,UAAM,IAAI,MAAM,uDAAuD;AAAA,EACzE;AAEA,SAAO;AACT;","names":["createContext","useContext","jsx","createContext","useContext"]}
|