1/* global posthog, Alpine, CustomEvent, htmx, localStorage, requestAnimationFrame */ 2 3/** 4 * PostHog event tracking 5 * Ensure we capture page views and page leaves for all pages 6 */ 7document.addEventListener('DOMContentLoaded', function () { 8 htmx.config.historyCacheSize = 0 9 // Check if PostHog is available and initialized 10 if (typeof posthog !== 'undefined' && posthog.get_distinct_id) { 11 // Capture initial page view 12 posthog.capture('$pageview', { 13 $current_url: window.location.href, 14 path: window.location.pathname, 15 referrer: document.referrer, 16 load_type: 'initial' 17 }) 18 19 // Set up page leave event 20 window.addEventListener('beforeunload', function () { 21 posthog.capture('$pageleave', { 22 $current_url: window.location.href, 23 path: window.location.pathname 24 }) 25 }) 26 } 27}) 28 29// Handle HTMX navigation to prevent sidebar flicker 30document.body.addEventListener('htmx:beforeSwap', function (event) { 31 // Only for boosted FULL-PAGE navigation (target = <body>). Boosted links that 32 // swap a scoped region (e.g. the docs nav targeting #docs-content) leave the 33 // rest of the page untouched, so none of this teardown work should run. 34 if (event.detail.boosted && event.detail.target === document.body) { 35 const leftNav = document.getElementById('left-nav') 36 if (leftNav) { 37 // Temporarily disable transitions to prevent animation during swap 38 leftNav.style.transition = 'none' 39 } 40 41 // Hide cookie icon during navigation to prevent it from appearing during transitions 42 const cookieIcon = document.getElementById('silktide-cookie-icon') 43 if (cookieIcon) { 44 cookieIcon.classList.remove('show-cookie-icon') 45 } 46 47 // Prevent scrollbar flash during swap by hiding overflow 48 document.documentElement.style.overflow = 'hidden' 49 50 // Clean up Alpine teleported elements (they live in <body> and survive hx-boost swaps) 51 document.querySelectorAll('[x-anchor]').forEach(el => el.remove()) 52 } 53}) 54 55// For HTMX-based navigation 56document.body.addEventListener('htmx:afterSettle', function (event) { 57 if (event.detail.target.id === 'modal') { 58 const _event = new CustomEvent('show-modal-event', { }) 59 window.dispatchEvent(_event) 60 } 61 62 // Handle partial swaps (hx-select pattern) for agent content areas 63 // These don't trigger boosted navigation, so we need to manually init Alpine 64 // Check if the request used hx-select targeting one of our content areas 65 const requestConfig = event.detail.requestConfig 66 const hxSelect = requestConfig?.elt?.getAttribute('hx-select') 67 const hxTarget = requestConfig?.elt?.getAttribute('hx-target') 68 const contentTargets = ['#segmenter-content', '#classifier-content', '#extractor-content'] 69 70 // Check both hx-select and hx-target to catch content swaps 71 const targetSelector = hxSelect || hxTarget 72 if (targetSelector && contentTargets.includes(targetSelector) && !event.detail.boosted) { 73 const contentEl = document.querySelector(targetSelector) 74 if (contentEl && typeof Alpine !== 'undefined') { 75 // Re-trigger alpine:init event to ensure all components are re-registered 76 document.dispatchEvent(new CustomEvent('alpine:init')) 77 // Initialize Alpine only on the swapped content 78 Alpine.initTree(contentEl) 79 // Re-process HTMX attributes on the new content 80 if (typeof htmx !== 'undefined') { 81 htmx.process(contentEl) 82 } 83 } 84 } 85 86 // Ensure sidebar state is synced with data attribute during navigation 87 if (event.detail.boosted) { 88 const sidebarFolded = localStorage.getItem('sidebarIsFolded') === 'true' 89 document.documentElement.setAttribute('data-sidebar-folded', sidebarFolded) 90 } 91 92 // Capture page view after HTMX content is loaded for PostHog analytics 93 if (typeof posthog !== 'undefined' && posthog.get_distinct_id && 94 event.detail.pathInfo && event.detail.pathInfo.path) { 95 // Use a small timeout to ensure the page has fully rendered for analytics 96 setTimeout(() => { 97 posthog.capture('$pageview', { 98 $current_url: window.location.href, 99 path: event.detail.pathInfo.path, 100 load_type: 'htmx' 101 }) 102 }, 100) // Timeout ensures analytics capture happens after rendering 103 } 104 105 // Handle boosted FULL-PAGE changes (target = <body>) - Alpine auto-initializes 106 // new content, so we only need to handle sidebar state and transitions. 107 // Scoped boosted swaps (docs nav â #docs-content) skip this: Alpine's 108 // MutationObserver picks up the swapped subtree and htmx processes it natively. 109 if (event.detail.boosted && event.detail.target === document.body) { 110 if (typeof Alpine !== 'undefined') { 111 // Store and restore sidebar state 112 const currentSidebarState = Alpine.$data(document.documentElement) 113 const preservedIsFolded = currentSidebarState?.isFolded 114 115 // Re-trigger alpine:init to ensure components are registered for new content 116 // Alpine will auto-init the new DOM, but we need components to be registered first 117 document.dispatchEvent(new CustomEvent('alpine:init')) 118 119 // Wait for Alpine's auto-initialization to complete 120 setTimeout(() => { 121 // Re-process HTMX attributes after Alpine reinit 122 // This ensures hx-* attributes on new DOM elements are recognized 123 if (typeof htmx !== 'undefined') { 124 htmx.process(document.body) 125 } 126 127 // After re-initialization, ensure the sidebar state is immediately restored 128 // This prevents the visual flicker by setting the state before the next frame 129 if (preservedIsFolded !== undefined) { 130 const rootData = Alpine.$data(document.documentElement) 131 if (rootData && 'isFolded' in rootData) { 132 rootData.isFolded = preservedIsFolded 133 } 134 } 135 136 // Re-enable transitions after Alpine is re-initialized 137 const leftNav = document.getElementById('left-nav') 138 if (leftNav) { 139 // Use requestAnimationFrame to ensure the DOM has settled 140 requestAnimationFrame(() => { 141 // Remove opacity-0 after Alpine init to prevent layout flash during hydration 142 document.body.classList.remove('opacity-0') 143 // Restore overflow after layout has settled 144 document.documentElement.style.overflow = '' 145 146 leftNav.style.transition = '' 147 148 // Show cookie icon again after navigation is complete 149 const cookieIcon = document.getElementById('silktide-cookie-icon') 150 if (cookieIcon) { 151 // Only show if the user has already made a cookie choice 152 const hasInitialChoice = localStorage.getItem('silktideCookieBanner_InitialChoice') !== null 153 if (hasInitialChoice) { 154 cookieIcon.classList.add('show-cookie-icon') 155 } 156 } 157 }) 158 } else { 159 // No left nav, still need to reveal body 160 requestAnimationFrame(() => { 161 document.body.classList.remove('opacity-0') 162 document.documentElement.style.overflow = '' 163 }) 164 } 165 }, 0) 166 } else {
167 console.error('Alpine is not defined. Cannot re-initialize after htmx:afterSettle.') 168 // Still reveal body even if Alpine is not available 169 document.body.classList.remove('opacity-0') 170 document.documentElement.style.overflow = '' 171 } 172 } 173}) 174 175/** 176 * Modal helper. 177 * Ensure we show backdrop and loading before modal content is loaded. 178 */ 179document.body.addEventListener('htmx:beforeRequest', function (event) { 180 // Don't show loading bar for boosted elements 181 if (event.detail.boosted) { 182 return 183 } 184 185 // Check if loading bar exists first 186 const loadingBar = document.getElementById('loadingBar') 187 if (loadingBar) { 188 loadingBar.classList.remove('hidden') 189 loadingBar.classList.add('block') 190 } 191}) 192 193/** 194 * Catch and display 500 with HTMx 195 */ 196document.body.addEventListener('htmx:responseError', function (event) { 197 if (event.detail.xhr.status === 500) { 198 const body = document.querySelector('body') 199 body.innerHTML = event.detail.xhr.responseText 200 } 201 // if 503 close modals and reload page 202 if (event.detail.xhr.status === 503) { 203 console.info('HTMx 503, reloading page') 204 window.location.reload() 205 } 206}) 207 208document.body.addEventListener('htmx:afterRequest', function (event) { 209 // Only hide loading bar if it wasn't a boosted request 210 if (!event.detail.boosted) { 211 const loadingBar = document.getElementById('loadingBar') 212 if (loadingBar) { 213 loadingBar.classList.add('hidden') 214 loadingBar.classList.remove('show') 215 } 216 } 217}) 218 219/** 220 * Reveal body after a short delay to prevent FOUC with Alpine. 221 * 222 * base.html no longer ships `opacity-0` on <body> â hiding every page until the 223 * whole script waterfall resolved cost a visible white flash on first paint. 224 * This stays as an opt-in: a page that genuinely needs a reveal adds `opacity-0` 225 * via {% block body_class %} and gets uncloaked here. Prefer x-cloak on the 226 * specific Alpine elements instead. 227 */ 228document.addEventListener('DOMContentLoaded', () => { 229 // Wait briefly for Alpine to initialize and apply x-cloak/localStorage states 230 setTimeout(() => { 231 document.body.classList.remove('opacity-0') 232 233 // Show cookie icon after page is fully loaded to prevent it from appearing during navigation 234 const cookieIcon = document.getElementById('silktide-cookie-icon') 235 if (cookieIcon && window.silktideCookieBannerManager) { 236 // Only show if the user has already made a cookie choice and the icon should be visible 237 const hasInitialChoice = localStorage.getItem('silktideCookieBanner_InitialChoice') !== null 238 if (hasInitialChoice) { 239 cookieIcon.classList.add('show-cookie-icon') 240 } 241 } 242 }, 50) // 50ms delay, adjust if needed 243}) 244 245// Ensure holofin namespace exists (without overwriting existing properties) 246window.holofin = window.holofin || {} 247 248// Track the last-seen path so the popstate handler can tell real page 249// traversal apart from same-document fragment navigation: clicking a #anchor 250// link fires popstate in Chrome/Firefox, and the reload below would turn every 251// in-page table-of-contents click into a full white-flash page reload. 252let lastNavigationPath = window.location.pathname + window.location.search 253 254// htmx pushes URLs without firing popstate â keep the snapshot current so the 255// next popstate compares against where htmx actually left us. 256document.body.addEventListener('htmx:pushedIntoHistory', function () { 257 lastNavigationPath = window.location.pathname + window.location.search 258}) 259 260// Listen for popstate event (browser back/forward button) 261window.addEventListener('popstate', function (event) { 262 const navigationPath = window.location.pathname + window.location.search 263 const isFragmentNavigation = navigationPath === lastNavigationPath 264 lastNavigationPath = navigationPath 265 if (isFragmentNavigation) { 266 // Only the #hash changed: the browser scrolls natively and all JS state 267 // (Alpine, Vue) is still valid â resetting or reloading here is what used 268 // to cause the blink on anchor links. 269 return 270 } 271 272 // Check if we're on extraction or document pages 273 const currentPath = window.location.pathname 274 const isExtractionPage = currentPath.includes('/extractor/') 275 const isDocumentPage = currentPath.includes('/documents/') 276 277 if (isExtractionPage || isDocumentPage) { 278 // For these pages, hide content immediately to prevent visual glitches 279 const extractionApp = document.getElementById('document-extraction-app') 280 const documentViewerApp = document.getElementById('document-viewer-app-container') 281 282 if (extractionApp) { 283 extractionApp.style.display = 'none' 284 } 285 if (documentViewerApp) { 286 documentViewerApp.style.display = 'none' 287 } 288 289 // Clean up Vue/Pinia apps if they exist 290 if (window.holofin?.extractionApp?.destroy) {
291 try { 292 window.holofin.extractionApp.destroy() 293 } catch (e) { 294 console.warn('Error destroying extraction app:', e) 295 } 296 } 297 298 // Allow a small delay for cleanup then reload 299 setTimeout(() => { 300 window.location.reload() 301 }, 10) 302 return 303 } 304 305 // For any page with Alpine components, ensure proper cleanup and reinitialization 306 // This provides the "dumbest Alpine context reset" to avoid stale state issues 307 if (typeof Alpine !== 'undefined') { 308 // Hide content containers immediately to prevent flash of stale content 309 const segmenterContent = document.getElementById('segmenter-content') 310 const classifierContent = document.getElementById('classifier-content') 311 const extractorContent = document.getElementById('extractor-content') 312 313 if (segmenterContent) segmenterContent.style.visibility = 'hidden' 314 if (classifierContent) classifierContent.style.visibility = 'hidden' 315 if (extractorContent) extractorContent.style.visibility = 'hidden' 316 317 // Destroy the current Alpine tree to clean up all components 318 Alpine.destroyTree(document.body) 319 320 // Force a full page reload to ensure clean state 321 // This is the safest approach to avoid any stale Alpine state 322 window.location.reload() 323 } 324}) 325 326// Additional safeguard for browser navigation 327window.addEventListener('pageshow', function (event) { 328 // Skip for extraction and document pages - they handle their own cleanup 329 const currentPath = window.location.pathname 330 const skipPaths = ['/extractor/', '/documents/'] 331 const shouldSkip = skipPaths.some(path => currentPath.includes(path)) 332 333 if (shouldSkip) { 334 return 335 } 336 337 // If the page is being shown from the back-forward cache (persisted) 338 if (event.persisted) { 339 // Force a reload to ensure clean Alpine state 340 window.location.reload() 341 } 342}) 343 344// Handle tab visibility change (user returns to tab after being away) 345// This fixes sidebar state desync when browser freezes/unfreezes tabs 346document.addEventListener('visibilitychange', function () { 347 if (document.visibilityState === 'visible') { 348 // Sync Alpine sidebar state with localStorage when tab becomes visible 349 if (typeof Alpine !== 'undefined') { 350 const storedFolded = localStorage.getItem('sidebarIsFolded') === 'true' 351 const rootData = Alpine.$data(document.documentElement) 352 353 if (rootData && 'isFolded' in rootData && rootData.isFolded !== storedFolded) { 354 // Only update if there's a mismatch 355 rootData.isFolded = storedFolded 356 document.documentElement.setAttribute('data-sidebar-folded', storedFolded) 357 } 358 } 359 } 360})
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.