@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/contexts/knowledge-base-session/storage.ts","../src/contexts/knowledge-base-session/notify.ts","../src/contexts/KnowledgeBaseSessionContext.tsx","../src/contexts/knowledge-base-session/refresh.ts","../src/components/Toast.tsx","../src/contexts/OpenResourcesContext.tsx","../src/contexts/TranslationContext.tsx"],"sourcesContent":["/**\n * Pure helpers for the KnowledgeBaseSession provider.\n *\n * Contains:\n * - localStorage shape and read/write helpers for KB list, active KB id,\n * and per-KB sessions\n * - JWT expiry parsing and \"is expired\" check\n * - URL/protocol helpers for KB instances\n * - The public `getKbSessionStatus(kbId)` helper that the KB-list UI uses\n * to color status dots without subscribing to context changes\n *\n * No React imports, no module-scoped state, no side effects beyond\n * localStorage. Splitting these out of the provider file makes them\n * unit-testable in isolation and keeps the React provider focused on\n * lifecycle and state.\n */\n\nimport type { KnowledgeBase, KbSessionStatus } from '../../types/knowledge-base';\n\n// ---------- Storage keys ----------\n\nconst SESSION_PREFIX = 'semiont.session.';\nexport const STORAGE_KEY = 'semiont.knowledgeBases';\nexport const ACTIVE_KEY = 'semiont.activeKnowledgeBaseId';\n\n/** Refresh the access token this many milliseconds before it expires. */\nexport const REFRESH_BEFORE_EXP_MS = 5 * 60 * 1000;\n\n/** The shape persisted to localStorage per KB. */\nexport interface StoredSession {\n access: string;\n refresh: string;\n}\n\nexport function sessionKey(kbId: string): string {\n return `${SESSION_PREFIX}${kbId}`;\n}\n\n// ---------- Per-KB session storage ----------\n\nexport function getStoredSession(kbId: string): StoredSession | null {\n const raw = localStorage.getItem(sessionKey(kbId));\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed.access === 'string' && typeof parsed.refresh === 'string') {\n return { access: parsed.access, refresh: parsed.refresh };\n }\n } catch {\n // malformed entry — treat as no session\n }\n return null;\n}\n\nexport function setStoredSession(kbId: string, session: StoredSession): void {\n localStorage.setItem(sessionKey(kbId), JSON.stringify(session));\n}\n\nexport function clearStoredSession(kbId: string): void {\n localStorage.removeItem(sessionKey(kbId));\n}\n\n// ---------- JWT helpers ----------\n\nexport function parseJwtExpiry(token: string): Date | null {\n try {\n const parts = token.split('.');\n if (parts.length !== 3 || !parts[1]) return null;\n const payload = JSON.parse(atob(parts[1])) as { exp?: number };\n if (!payload.exp) return null;\n return new Date(payload.exp * 1000);\n } catch {\n return null;\n }\n}\n\nexport function isJwtExpired(token: string): boolean {\n const expiry = parseJwtExpiry(token);\n if (!expiry) return true;\n return expiry.getTime() < Date.now();\n}\n\n// ---------- KB list storage ----------\n\nfunction migrateLegacyEntry(entry: any): KnowledgeBase {\n if (entry.host !== undefined) return entry as KnowledgeBase;\n // Legacy format: { id, label, backendUrl }\n try {\n const url = new URL(entry.backendUrl);\n return {\n id: entry.id,\n label: entry.label,\n host: url.hostname,\n port: parseInt(url.port, 10) || (url.protocol === 'https:' ? 443 : 80),\n protocol: url.protocol === 'https:' ? 'https' : 'http',\n email: '',\n };\n } catch {\n return {\n id: entry.id,\n label: entry.label || 'Unknown',\n host: 'localhost',\n port: 4000,\n protocol: 'http',\n email: '',\n };\n }\n}\n\nexport function loadKnowledgeBases(): KnowledgeBase[] {\n try {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (!raw) return [];\n const entries = JSON.parse(raw) as any[];\n return entries.map(migrateLegacyEntry);\n } catch {\n return [];\n }\n}\n\nexport function saveKnowledgeBases(knowledgeBases: KnowledgeBase[]): void {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(knowledgeBases));\n}\n\n// ---------- Public pure helpers ----------\n\nexport function defaultProtocol(host: string): 'http' | 'https' {\n return host === 'localhost' || host === '127.0.0.1' ? 'http' : 'https';\n}\n\nexport function kbBackendUrl(kb: KnowledgeBase): string {\n return `${kb.protocol}://${kb.host}:${kb.port}`;\n}\n\n/**\n * Read the locally-stored credential status for a KB. Pure / synchronous —\n * does not subscribe to context changes. Used by KB-list UI to color status\n * dots without requiring re-renders on every tick.\n */\nexport function getKbSessionStatus(kbId: string): KbSessionStatus {\n const stored = getStoredSession(kbId);\n if (!stored) return 'signed-out';\n return isJwtExpired(stored.access) ? 'expired' : 'authenticated';\n}\n\nexport function generateKbId(): string {\n return crypto.randomUUID();\n}\n","/**\n * Module-scoped session-expired / permission-denied notifier.\n *\n * The provider registers itself with this module-scoped slot on mount and\n * unregisters on unmount via {@link registerAuthNotifyHandlers}. Code outside\n * the React tree (notably the React Query QueryCache.onError handler in app\n * providers) calls {@link notifySessionExpired} or {@link notifyPermissionDenied}\n * to reach the active provider.\n *\n * When no provider is mounted (e.g. on the landing page), these calls are\n * no-ops — there is nothing to notify.\n *\n * No React imports — this is plain module state. The provider effect that\n * calls `registerAuthNotifyHandlers` runs inside React but the module itself\n * is React-agnostic.\n */\n\ntype Notify = (message?: string) => void;\n\nlet activeOnSessionExpired: Notify | null = null;\nlet activeOnPermissionDenied: Notify | null = null;\n\nexport function notifySessionExpired(message?: string): void {\n activeOnSessionExpired?.(message);\n}\n\nexport function notifyPermissionDenied(message?: string): void {\n activeOnPermissionDenied?.(message);\n}\n\n/**\n * Install handlers for session-expired and permission-denied notifications.\n * Returns an unregister callback. Intended to be called from a React useEffect\n * with the cleanup callback returned from the effect.\n *\n * Only one provider is expected to be mounted at a time. If a second provider\n * mounts before the first unmounts, its handlers replace the previous ones —\n * the previous provider becomes deaf to notifications. In practice this only\n * happens during the brief window of a React StrictMode double-mount or a\n * test that mounts and unmounts multiple providers rapidly.\n */\nexport function registerAuthNotifyHandlers(handlers: {\n onSessionExpired: Notify;\n onPermissionDenied: Notify;\n}): () => void {\n activeOnSessionExpired = handlers.onSessionExpired;\n activeOnPermissionDenied = handlers.onPermissionDenied;\n return () => {\n activeOnSessionExpired = null;\n activeOnPermissionDenied = null;\n };\n}\n","/**\n * KnowledgeBaseSessionContext — single source of truth for \"which KB and\n * what's the session against it.\"\n *\n * This provider merges what used to be three separate concerns in the\n * frontend (KnowledgeBaseProvider + KnowledgeBaseAuthBridge + AuthProvider)\n * plus the library-side SessionProvider, into one coherent unit.\n *\n * Why merged: a session in this app is always a session against a specific\n * KB. There is no auth without a KB. Switching KBs means switching sessions\n * atomically.\n *\n * What it owns:\n * - The list of configured KBs (persisted to localStorage)\n * - Which KB is currently active (persisted to localStorage)\n * - The validated session (token + user) for the active KB\n * - The \"session expired\" and \"permission denied\" flags that drive the modals\n * - JWT expiry derivations (for the session-timer UI)\n * - Mount-time validation flow with manual 401 recovery\n * - Proactive refresh: a timer that fires before the access token expires\n * - Cross-tab sync: when another tab refreshes or signs out, this tab updates\n *\n * Implementation is split across the `knowledge-base-session/` directory:\n * - `storage.ts` — localStorage shape, JWT helpers, KB list helpers\n * - `refresh.ts` — `performRefresh` and the in-flight Promise dedup map\n * - `notify.ts` — module-scoped notify functions and the register helper\n *\n * Mounting: must be inside `EventBusProvider` and `TranslationProvider` (it\n * uses neither, but the modals it sits next to do). It does NOT depend on\n * any other library context. Mount it inside the protected layout boundary.\n */\n\nimport React, {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { SemiontApiClient, APIError } from '@semiont/api-client';\nimport { baseUrl, EventBus, accessToken } from '@semiont/core';\nimport type { components } from '@semiont/core';\nimport type {\n KnowledgeBase,\n NewKnowledgeBase,\n} from '../types/knowledge-base';\nimport {\n ACTIVE_KEY,\n REFRESH_BEFORE_EXP_MS,\n clearStoredSession,\n generateKbId,\n getStoredSession,\n isJwtExpired,\n kbBackendUrl,\n loadKnowledgeBases,\n parseJwtExpiry,\n saveKnowledgeBases,\n sessionKey,\n setStoredSession,\n} from './knowledge-base-session/storage';\nimport { performRefresh } from './knowledge-base-session/refresh';\nimport { registerAuthNotifyHandlers } from './knowledge-base-session/notify';\nimport type { StoredSession } from './knowledge-base-session/storage';\n\ntype UserInfo = components['schemas']['UserResponse'];\n\nexport interface AuthSession {\n token: string;\n user: UserInfo;\n}\n\n// Re-export the public surface so consumers can keep importing from this module\nexport {\n defaultProtocol,\n kbBackendUrl,\n getKbSessionStatus,\n} from './knowledge-base-session/storage';\nexport type { StoredSession } from './knowledge-base-session/storage';\nexport {\n notifySessionExpired,\n notifyPermissionDenied,\n} from './knowledge-base-session/notify';\n\n// ---------- Context value ----------\n\ninterface KnowledgeBaseSessionValue {\n // KB list\n knowledgeBases: KnowledgeBase[];\n activeKnowledgeBase: KnowledgeBase | null;\n\n // Session state for the active KB\n session: AuthSession | null;\n isLoading: boolean;\n\n // Derived auth fields (memoized off `session.user`)\n user: UserInfo | null;\n token: string | null;\n isAuthenticated: boolean;\n hasValidBackendToken: boolean;\n isFullyAuthenticated: boolean;\n displayName: string;\n avatarUrl: string | null;\n userDomain: string | undefined;\n isAdmin: boolean;\n isModerator: boolean;\n\n // JWT expiry (derived from session.token)\n expiresAt: Date | null;\n\n // Modal-driving flags\n sessionExpiredAt: number | null;\n sessionExpiredMessage: string | null;\n permissionDeniedAt: number | null;\n permissionDeniedMessage: string | null;\n\n // Mutations\n addKnowledgeBase: (kb: NewKnowledgeBase, access: string, refresh: string) => KnowledgeBase;\n removeKnowledgeBase: (id: string) => void;\n setActiveKnowledgeBase: (id: string) => void;\n updateKnowledgeBase: (id: string, updates: Partial<Pick<KnowledgeBase, 'label'>>) => void;\n /** Re-auth on an existing KB: store the new tokens and refresh the session. */\n signIn: (id: string, access: string, refresh: string) => void;\n /** Sign out of a KB: clear its stored tokens. If it's the active KB, clear in-memory session too. */\n signOut: (id: string) => void;\n\n /**\n * Refresh the active KB's access token. Returns the new access token, or\n * null if no refresh token is available or the refresh failed. Concurrent\n * calls deduplicate via an in-flight Promise per KB. Used by the api-client's\n * 401-recovery hook and by the proactive refresh timer.\n */\n refreshActive: () => Promise<string | null>;\n\n // Modal acks\n acknowledgeSessionExpired: () => void;\n acknowledgePermissionDenied: () => void;\n}\n\n/**\n * Raw context export. Exposed for test utilities that need to construct\n * a mock provider without going through localStorage and JWT validation.\n * Production code should always use {@link useKnowledgeBaseSession} instead.\n */\nexport const KnowledgeBaseSessionContext = createContext<KnowledgeBaseSessionValue | undefined>(undefined);\n\nexport type { KnowledgeBaseSessionValue };\n\n// ---------- Provider ----------\n\nexport function KnowledgeBaseSessionProvider({ children }: { children: React.ReactNode }) {\n // KB list and active selection\n const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>(() => loadKnowledgeBases());\n const [activeKnowledgeBaseId, setActiveKnowledgeBaseId] = useState<string | null>(() => {\n const saved = localStorage.getItem(ACTIVE_KEY);\n const loaded = loadKnowledgeBases();\n if (saved && loaded.some(kb => kb.id === saved)) return saved;\n return loaded[0]?.id ?? null;\n });\n\n // Session state for the active KB\n const [session, setSession] = useState<AuthSession | null>(null);\n const [isLoading, setIsLoading] = useState<boolean>(() => {\n const id = activeKnowledgeBaseId;\n if (!id) return false;\n const stored = getStoredSession(id);\n if (!stored) return false;\n // We'll either validate (if access fresh) or refresh (if refresh available)\n return !isJwtExpired(stored.access) || stored.refresh != null;\n });\n\n // Modal flags\n const [sessionExpiredAt, setSessionExpiredAt] = useState<number | null>(null);\n const [sessionExpiredMessage, setSessionExpiredMessage] = useState<string | null>(null);\n const [permissionDeniedAt, setPermissionDeniedAt] = useState<number | null>(null);\n const [permissionDeniedMessage, setPermissionDeniedMessage] = useState<string | null>(null);\n\n // Persist KB list and active id\n useEffect(() => {\n saveKnowledgeBases(knowledgeBases);\n }, [knowledgeBases]);\n\n useEffect(() => {\n if (activeKnowledgeBaseId) {\n localStorage.setItem(ACTIVE_KEY, activeKnowledgeBaseId);\n } else {\n localStorage.removeItem(ACTIVE_KEY);\n }\n }, [activeKnowledgeBaseId]);\n\n const activeKnowledgeBase = useMemo(\n () => knowledgeBases.find(kb => kb.id === activeKnowledgeBaseId) ?? null,\n [knowledgeBases, activeKnowledgeBaseId]\n );\n\n // Refs for cross-effect coordination\n const activeKbRef = useRef<KnowledgeBase | null>(activeKnowledgeBase);\n useEffect(() => {\n activeKbRef.current = activeKnowledgeBase;\n }, [activeKnowledgeBase]);\n\n const proactiveRefreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n /**\n * Schedule a one-shot timer that fires `REFRESH_BEFORE_EXP_MS` before the\n * given access token expires. Cancels any prior pending timer.\n */\n const scheduleProactiveRefresh = useCallback((accessTokenStr: string) => {\n if (proactiveRefreshTimerRef.current) {\n clearTimeout(proactiveRefreshTimerRef.current);\n proactiveRefreshTimerRef.current = null;\n }\n const expiresAt = parseJwtExpiry(accessTokenStr);\n if (!expiresAt) return;\n const refreshAt = expiresAt.getTime() - REFRESH_BEFORE_EXP_MS;\n const delay = Math.max(0, refreshAt - Date.now());\n proactiveRefreshTimerRef.current = setTimeout(() => {\n proactiveRefreshTimerRef.current = null;\n // Fire-and-forget: refreshActive captures activeKbRef and updates state\n refreshActiveRef.current?.();\n }, delay);\n }, []);\n\n // refreshActive needs to be stable across renders for the api-client wiring\n // and also needs to read fresh activeKnowledgeBase via the ref. We define\n // the function via a ref so callers can capture a stable reference.\n const refreshActiveRef = useRef<(() => Promise<string | null>) | null>(null);\n const refreshActive = useCallback(async (): Promise<string | null> => {\n const kb = activeKbRef.current;\n if (!kb) return null;\n const newAccess = await performRefresh(kb);\n if (newAccess) {\n // Update the in-memory session token so consumers see the new value\n setSession(prev => (prev ? { ...prev, token: newAccess } : prev));\n scheduleProactiveRefresh(newAccess);\n } else {\n // Refresh failed — surface the modal\n setSession(null);\n clearStoredSession(kb.id);\n setSessionExpiredMessage('Your session has expired. Please sign in again.');\n setSessionExpiredAt(Date.now());\n if (proactiveRefreshTimerRef.current) {\n clearTimeout(proactiveRefreshTimerRef.current);\n proactiveRefreshTimerRef.current = null;\n }\n }\n return newAccess;\n }, [scheduleProactiveRefresh]);\n refreshActiveRef.current = refreshActive;\n\n // Mount-time validation. This is the only 401-handling path that does NOT\n // go through the api-client's `tokenRefresher` hook. Two structural reasons:\n //\n // 1. ApiClientProvider hasn't mounted yet — the protected layout mounts\n // ApiClientProvider as a CHILD of this provider, so at validation time\n // the configured api-client (the one with `tokenRefresher`) doesn't\n // exist yet.\n //\n // 2. Even if it did, having the api-client silently recover would mean\n // this effect would never see the 401. But this effect is what BUILDS\n // the session — it needs to know whether validation succeeded so it\n // can either set `session = { token, user }` or surface the modal.\n //\n // So this effect uses a fresh throwaway api-client (no refresher) and\n // handles 401 manually: try one refresh, retry getMe with the new token,\n // surface the modal only if both fail. The duplication with the api-client's\n // beforeRetry hook is structural — do not try to consolidate them.\n useEffect(() => {\n if (!activeKnowledgeBase) {\n setSession(null);\n setIsLoading(false);\n return;\n }\n\n const stored = getStoredSession(activeKnowledgeBase.id);\n if (!stored) {\n setSession(null);\n setIsLoading(false);\n return;\n }\n\n let cancelled = false;\n\n const validate = async (tokenToUse: string) => {\n const client = new SemiontApiClient({\n baseUrl: baseUrl(kbBackendUrl(activeKnowledgeBase)),\n eventBus: new EventBus(),\n });\n try {\n const data = await client.getMe({ auth: accessToken(tokenToUse) });\n if (cancelled) return;\n setSession({ token: tokenToUse, user: data as UserInfo });\n scheduleProactiveRefresh(tokenToUse);\n } catch (error) {\n if (cancelled) return;\n setSession(null);\n if (error instanceof APIError && error.status === 401) {\n // Try one refresh on 401 from getMe before surfacing the modal\n const refreshed = await performRefresh(activeKnowledgeBase);\n if (cancelled) return;\n if (refreshed) {\n return validate(refreshed);\n }\n clearStoredSession(activeKnowledgeBase.id);\n setSessionExpiredMessage('Your session has expired. Please sign in again.');\n setSessionExpiredAt(Date.now());\n }\n } finally {\n if (!cancelled) setIsLoading(false);\n }\n };\n\n setIsLoading(true);\n\n if (isJwtExpired(stored.access)) {\n (async () => {\n const refreshed = await performRefresh(activeKnowledgeBase);\n if (cancelled) return;\n if (refreshed) {\n await validate(refreshed);\n } else {\n setSession(null);\n clearStoredSession(activeKnowledgeBase.id);\n setIsLoading(false);\n }\n })();\n } else {\n validate(stored.access);\n }\n\n return () => {\n cancelled = true;\n };\n }, [activeKnowledgeBase, scheduleProactiveRefresh]);\n\n // Cancel proactive refresh timer on unmount\n useEffect(() => {\n return () => {\n if (proactiveRefreshTimerRef.current) {\n clearTimeout(proactiveRefreshTimerRef.current);\n proactiveRefreshTimerRef.current = null;\n }\n };\n }, []);\n\n // Cross-tab sync: listen for storage events on the active KB's session key\n useEffect(() => {\n if (!activeKnowledgeBaseId) return;\n const watchKey = sessionKey(activeKnowledgeBaseId);\n const handler = (e: StorageEvent) => {\n if (e.key !== watchKey) return;\n if (!e.newValue) {\n // Token was cleared in another tab\n setSession(null);\n if (proactiveRefreshTimerRef.current) {\n clearTimeout(proactiveRefreshTimerRef.current);\n proactiveRefreshTimerRef.current = null;\n }\n return;\n }\n try {\n const parsed = JSON.parse(e.newValue) as StoredSession;\n if (typeof parsed.access === 'string') {\n // Update our in-memory session token (user info is unchanged)\n setSession(prev => (prev ? { ...prev, token: parsed.access } : prev));\n scheduleProactiveRefresh(parsed.access);\n }\n } catch {\n // Ignore malformed payloads\n }\n };\n window.addEventListener('storage', handler);\n return () => window.removeEventListener('storage', handler);\n }, [activeKnowledgeBaseId, scheduleProactiveRefresh]);\n\n // Register module-scoped notify handlers so the QueryCache 401/403 handlers\n // can reach the active provider instance. Returns a cleanup callback that\n // unregisters the handlers when the active KB id changes or the provider\n // unmounts.\n useEffect(() => {\n return registerAuthNotifyHandlers({\n onSessionExpired: (message) => {\n setSessionExpiredMessage(message ?? 'Your session has expired. Please sign in again.');\n setSessionExpiredAt(Date.now());\n setSession(null);\n if (activeKnowledgeBaseId) {\n clearStoredSession(activeKnowledgeBaseId);\n }\n if (proactiveRefreshTimerRef.current) {\n clearTimeout(proactiveRefreshTimerRef.current);\n proactiveRefreshTimerRef.current = null;\n }\n },\n onPermissionDenied: (message) => {\n setPermissionDeniedMessage(message ?? 'You do not have permission to perform this action.');\n setPermissionDeniedAt(Date.now());\n },\n });\n }, [activeKnowledgeBaseId]);\n\n // Mutations\n const addKnowledgeBase = useCallback((input: NewKnowledgeBase, access: string, refresh: string): KnowledgeBase => {\n const kb: KnowledgeBase = { id: generateKbId(), ...input };\n setStoredSession(kb.id, { access, refresh });\n setKnowledgeBases(prev => [...prev, kb]);\n setActiveKnowledgeBaseId(kb.id);\n return kb;\n }, []);\n\n const removeKnowledgeBase = useCallback((id: string) => {\n clearStoredSession(id);\n setKnowledgeBases(prev => {\n const remaining = prev.filter(kb => kb.id !== id);\n setActiveKnowledgeBaseId(activeId => activeId === id ? (remaining[0]?.id ?? null) : activeId);\n return remaining;\n });\n }, []);\n\n const setActiveKnowledgeBase = useCallback((id: string) => {\n setActiveKnowledgeBaseId(id);\n }, []);\n\n const updateKnowledgeBase = useCallback((id: string, updates: Partial<Pick<KnowledgeBase, 'label'>>) => {\n setKnowledgeBases(prev => prev.map(kb => kb.id === id ? { ...kb, ...updates } : kb));\n }, []);\n\n const signIn = useCallback((id: string, access: string, refresh: string) => {\n setStoredSession(id, { access, refresh });\n // Replace the matching KB with a fresh object so the activeKnowledgeBase\n // memo's `find()` returns a new reference, the validation effect re-runs,\n // and the new tokens get used.\n setKnowledgeBases(prev => prev.map(kb => kb.id === id ? { ...kb } : kb));\n setActiveKnowledgeBaseId(id);\n }, []);\n\n const signOut = useCallback((id: string) => {\n clearStoredSession(id);\n setActiveKnowledgeBaseId(activeId => {\n if (activeId === id) {\n setSession(null);\n if (proactiveRefreshTimerRef.current) {\n clearTimeout(proactiveRefreshTimerRef.current);\n proactiveRefreshTimerRef.current = null;\n }\n }\n return activeId;\n });\n // Bump the KB list so consumers reading kbStatus(id) see the change\n setKnowledgeBases(prev => [...prev]);\n }, []);\n\n const acknowledgeSessionExpired = useCallback(() => {\n setSessionExpiredAt(null);\n setSessionExpiredMessage(null);\n }, []);\n\n const acknowledgePermissionDenied = useCallback(() => {\n setPermissionDeniedAt(null);\n setPermissionDeniedMessage(null);\n }, []);\n\n // Tick state forces re-derivation of expiresAt-based fields once a minute,\n // so the session-timer UI updates without each consumer running its own interval.\n const [, setTick] = useState(0);\n useEffect(() => {\n const interval = setInterval(() => setTick(t => t + 1), 30_000);\n return () => clearInterval(interval);\n }, []);\n\n // Derived auth fields\n const value = useMemo<KnowledgeBaseSessionValue>(() => {\n const user = session?.user ?? null;\n const token = session?.token ?? null;\n const expiresAt = token ? parseJwtExpiry(token) : null;\n\n return {\n knowledgeBases,\n activeKnowledgeBase,\n session,\n isLoading,\n user,\n token,\n isAuthenticated: !!session,\n hasValidBackendToken: !!token,\n isFullyAuthenticated: !!session,\n displayName: user?.name ?? user?.email?.split('@')[0] ?? 'User',\n avatarUrl: user?.image ?? null,\n userDomain: user?.domain || user?.email?.split('@')[1],\n isAdmin: user?.isAdmin ?? false,\n isModerator: user?.isModerator ?? false,\n expiresAt,\n sessionExpiredAt,\n sessionExpiredMessage,\n permissionDeniedAt,\n permissionDeniedMessage,\n addKnowledgeBase,\n removeKnowledgeBase,\n setActiveKnowledgeBase,\n updateKnowledgeBase,\n signIn,\n signOut,\n refreshActive,\n acknowledgeSessionExpired,\n acknowledgePermissionDenied,\n };\n }, [\n knowledgeBases,\n activeKnowledgeBase,\n session,\n isLoading,\n sessionExpiredAt,\n sessionExpiredMessage,\n permissionDeniedAt,\n permissionDeniedMessage,\n addKnowledgeBase,\n removeKnowledgeBase,\n setActiveKnowledgeBase,\n updateKnowledgeBase,\n signIn,\n signOut,\n refreshActive,\n acknowledgeSessionExpired,\n acknowledgePermissionDenied,\n ]);\n\n return (\n <KnowledgeBaseSessionContext.Provider value={value}>\n {children}\n </KnowledgeBaseSessionContext.Provider>\n );\n}\n\n// ---------- Hook ----------\n\nexport function useKnowledgeBaseSession(): KnowledgeBaseSessionValue {\n const ctx = useContext(KnowledgeBaseSessionContext);\n if (!ctx) {\n throw new Error(\n 'useKnowledgeBaseSession requires KnowledgeBaseSessionProvider. ' +\n 'This component is rendered outside the auth boundary. ' +\n 'Move it into a protected layout.'\n );\n }\n return ctx;\n}\n","/**\n * Refresh-token coordination for the KnowledgeBaseSession provider.\n *\n * Module-scoped state: an in-flight Promise per KB so concurrent 401s for\n * the same KB deduplicate to a single network call. No React, no provider\n * dependency — the React provider calls `performRefresh(kb)` and the\n * api-client's `tokenRefresher` hook indirectly calls it via the provider's\n * `refreshActive` method.\n */\n\nimport { SemiontApiClient } from '@semiont/api-client';\nimport { baseUrl, EventBus, refreshToken as makeRefreshToken } from '@semiont/core';\nimport type { KnowledgeBase } from '../../types/knowledge-base';\nimport { getStoredSession, setStoredSession, kbBackendUrl } from './storage';\n\n/**\n * One in-flight refresh promise per KB. Ensures concurrent 401s for the same\n * KB deduplicate to a single network call.\n */\nconst inFlightRefreshes: Map<string, Promise<string | null>> = new Map();\n\n/**\n * Refresh the active KB's access token. Returns the new access token, or\n * null if no refresh token is available or the refresh failed.\n *\n * IMPORTANT: this constructs a fresh `SemiontApiClient` *without* a\n * `tokenRefresher`. Do not be tempted to reuse the configured client (e.g.\n * via `useApiClient()` from a layout): a refresh-call returning 401 would\n * recursively re-enter the refresher, calling `/api/tokens/refresh` again,\n * in an infinite loop. The throwaway client deliberately has no recovery\n * path — a 401 here propagates as `null` and surfaces the modal upstream.\n *\n * Concurrent calls for the same KB deduplicate via the in-flight Promise\n * Map keyed by `kb.id`, so simultaneous 401s on different requests trigger\n * only one network round-trip to `/api/tokens/refresh`.\n */\nexport async function performRefresh(kb: KnowledgeBase): Promise<string | null> {\n const existing = inFlightRefreshes.get(kb.id);\n if (existing) return existing;\n\n const promise = (async (): Promise<string | null> => {\n const stored = getStoredSession(kb.id);\n if (!stored) return null;\n\n const client = new SemiontApiClient({\n baseUrl: baseUrl(kbBackendUrl(kb)),\n eventBus: new EventBus(),\n });\n\n try {\n const response = await client.refreshToken(makeRefreshToken(stored.refresh));\n const newAccess = response.access_token;\n if (!newAccess) return null;\n setStoredSession(kb.id, { access: newAccess, refresh: stored.refresh });\n return newAccess;\n } catch {\n return null;\n }\n })();\n\n inFlightRefreshes.set(kb.id, promise);\n try {\n return await promise;\n } finally {\n inFlightRefreshes.delete(kb.id);\n }\n}\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":["SemiontApiClient","baseUrl","EventBus","SemiontApiClient","baseUrl","EventBus","React","useEffect","useState","jsx","useEffect","useState","React","createContext","useContext","jsx","createContext","useContext","useState","useEffect","useMemo","jsx","createContext","useState","useEffect","useMemo","jsx","useContext"]}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
useApiClient,
|
|
4
4
|
useEventBus
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-OZICDVH7.mjs";
|
|
6
6
|
|
|
7
7
|
// src/contexts/AuthTokenContext.tsx
|
|
8
8
|
import { createContext, useContext } from "react";
|
|
@@ -158,7 +158,7 @@ function useHoverEmitter(annotationId, hoverDelayMs = HOVER_DELAY_MS) {
|
|
|
158
158
|
return { onMouseEnter, onMouseLeave };
|
|
159
159
|
}
|
|
160
160
|
function useAttentionStream() {
|
|
161
|
-
const
|
|
161
|
+
const semiont = useApiClient();
|
|
162
162
|
const token = useAuthToken();
|
|
163
163
|
const tokenRef = useRef2(token);
|
|
164
164
|
useEffect2(() => {
|
|
@@ -168,12 +168,13 @@ function useAttentionStream() {
|
|
|
168
168
|
useEffect2(() => {
|
|
169
169
|
setStatus("connecting");
|
|
170
170
|
try {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
const stream = semiont.sse.attentionStream({
|
|
172
|
+
auth: tokenRef.current ? accessToken(tokenRef.current) : void 0,
|
|
173
|
+
eventBus: semiont.eventBus
|
|
174
|
+
});
|
|
174
175
|
setStatus("connected");
|
|
175
176
|
return () => {
|
|
176
|
-
|
|
177
|
+
stream.close();
|
|
177
178
|
setStatus("disconnected");
|
|
178
179
|
};
|
|
179
180
|
} catch (error) {
|
|
@@ -181,7 +182,7 @@ function useAttentionStream() {
|
|
|
181
182
|
setStatus("error");
|
|
182
183
|
return;
|
|
183
184
|
}
|
|
184
|
-
}, [
|
|
185
|
+
}, [semiont]);
|
|
185
186
|
return { status };
|
|
186
187
|
}
|
|
187
188
|
|
|
@@ -196,4 +197,4 @@ export {
|
|
|
196
197
|
useHoverEmitter,
|
|
197
198
|
useAttentionStream
|
|
198
199
|
};
|
|
199
|
-
//# sourceMappingURL=chunk-
|
|
200
|
+
//# sourceMappingURL=chunk-VN5NY4SN.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
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:create-ok', ({ annotationId }) => {\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:create-ok': ({ annotationId }) => handleCreated(annotationId),\n * 'mark:delete-ok': ({ 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 semiont = 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 stream = semiont.sse.attentionStream({\n auth: tokenRef.current ? accessToken(tokenRef.current) : undefined,\n eventBus: semiont.eventBus,\n });\n setStatus('connected');\n return () => { stream.close(); setStatus('disconnected'); };\n } catch (error) {\n console.error('[AttentionStream] Failed to connect:', error);\n setStatus('error');\n return;\n }\n }, [semiont]);\n\n return { status };\n}\n"],"mappings":"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","names":["subscriptions","useRef","useEffect","useRef","useEffect"]}
|