/** * Library Desk Integration for Wiki.js * Combined re-index and entity linking buttons * * Usage: Add to Wiki.js Code Injection: * */ (function() { 'use strict'; // Auto-detect Library Desk URL const scriptTag = document.currentScript; const scriptUrl = scriptTag ? scriptTag.src : ''; const libraryDeskUrl = scriptUrl ? scriptUrl.split('/static/')[0] : 'http://192.168.86.149:8089'; // Shared configuration const CONFIG = window.LIBRARY_DESK_CONFIG || { libraryDeskUrl: libraryDeskUrl, apiKey: 'af88ed8f44bed81bdb20d0534f1c4547340b29e2aba4963f61a71b993d7eb6e5', user: 'jpmschweitzer', buttonPosition: 'toolbar', // 'toolbar' or 'floating' debug: true }; function log() { if (CONFIG.debug) { console.log('[Library Desk]', ...arguments); } } // Store page ID globally once fetched let CACHED_PAGE_ID = null; let CACHED_PAGE_PATH = null; // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } function init() { log('Initializing Library Desk integration...'); log('Library Desk URL:', CONFIG.libraryDeskUrl); // Wait for page to be ready, then add buttons setTimeout(addButtons, 1500); } async function addButtons() { const pagePath = getPagePath(); if (!pagePath) { log('Skipping buttons - not on a content page'); return; } log('Page path:', pagePath); // Check if buttons already exist if (document.getElementById('library-desk-buttons')) { log('Buttons already exist'); return; } // Fetch and cache page ID during initialization CACHED_PAGE_PATH = pagePath; CACHED_PAGE_ID = await getPageIdFromPath(pagePath); log('Cached page ID for this session:', CACHED_PAGE_ID); if (CONFIG.buttonPosition === 'floating') { addFloatingButtons(pagePath); } else { addToolbarButtons(pagePath); } } function addToolbarButtons(pagePath) { const toolbar = findToolbar(); if (!toolbar) { log('Toolbar not found, falling back to floating buttons'); addFloatingButtons(pagePath); return; } // Container for both buttons const btnContainer = document.createElement('div'); btnContainer.id = 'library-desk-buttons'; btnContainer.style.cssText = 'display: inline-flex; align-items: center; gap: 4px; margin-left: 8px;'; // Re-index button const reindexBtn = createToolbarButton( 'library-desk-reindex-btn', 'Re-index this page in Library Desk (vectors + knowledge graph)', 'mdi-database-sync', () => reindexPage(pagePath, reindexBtn) ); // Entity link button const entityLinkBtn = createToolbarButton( 'library-desk-entitylink-btn', 'Link entities mentioned in this page to the knowledge graph', 'mdi-graph-outline', () => linkEntities(pagePath, entityLinkBtn) ); btnContainer.appendChild(reindexBtn); btnContainer.appendChild(entityLinkBtn); toolbar.appendChild(btnContainer); log('Toolbar buttons added'); } function createToolbarButton(id, title, icon, onClick) { const button = document.createElement('button'); button.id = id; button.className = 'v-btn v-btn--icon v-btn--round theme--dark v-size--default'; button.type = 'button'; button.title = title; button.setAttribute('aria-label', title); button.innerHTML = ``; button.addEventListener('mouseenter', function() { this.style.backgroundColor = 'rgba(255, 255, 255, 0.08)'; }); button.addEventListener('mouseleave', function() { this.style.backgroundColor = ''; }); button.onclick = function(e) { e.preventDefault(); e.stopPropagation(); onClick(); }; return button; } function addFloatingButtons(pagePath) { const container = document.createElement('div'); container.id = 'library-desk-buttons'; container.style.cssText = ` position: fixed; bottom: 20px; right: 20px; display: flex; flex-direction: column; gap: 10px; z-index: 9999; `; // Re-index button const reindexBtn = createFloatingButton( 'library-desk-reindex-btn', '🔄 Re-index', 'Re-index this page in Library Desk', '#1976d2', '#1565c0', () => reindexPage(pagePath, reindexBtn) ); // Entity link button const entityLinkBtn = createFloatingButton( 'library-desk-entitylink-btn', '🔗 Link Entities', 'Link entities in this page', '#43a047', '#388e3c', () => linkEntities(pagePath, entityLinkBtn) ); container.appendChild(reindexBtn); container.appendChild(entityLinkBtn); document.body.appendChild(container); log('Floating buttons added'); } function createFloatingButton(id, text, title, bgColor, hoverColor, onClick) { const button = document.createElement('button'); button.id = id; button.innerHTML = text; button.title = title; button.style.cssText = ` padding: 10px 15px; background: ${bgColor}; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: 500; box-shadow: 0 2px 8px rgba(0,0,0,0.3); transition: all 0.3s; `; button.onmouseover = function() { this.style.background = hoverColor; }; button.onmouseout = function() { this.style.background = bgColor; }; button.onclick = onClick; return button; } // ============================================================================ // RE-INDEX FUNCTIONALITY // ============================================================================ async function reindexPage(pagePath, button) { const isFloating = button.id === 'library-desk-reindex-btn' && button.innerHTML.includes('Re-index'); const icon = isFloating ? null : button.querySelector('.v-icon'); const originalContent = isFloating ? button.innerHTML : null; const originalIcon = icon ? icon.className : null; button.disabled = true; if (isFloating) { button.innerHTML = '⏳ Loading...'; button.style.background = '#757575'; } else { icon.className = 'v-icon notranslate mdi mdi-loading mdi-spin theme--dark'; } try { // Use cached page ID from initialization if (!CACHED_PAGE_ID) { throw new Error('Page ID not available - was not fetched during initialization'); } log('Re-indexing page', CACHED_PAGE_ID, '...'); // Re-index directly const response = await fetch(CONFIG.libraryDeskUrl + '/ingest/page', { method: 'POST', headers: { 'Authorization': 'Bearer ' + CONFIG.apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ user: CONFIG.user, page_id: CACHED_PAGE_ID, force_refresh: true }) }); const result = await response.json(); if (response.ok && result.success) { if (isFloating) { button.innerHTML = '✓ Done!'; button.style.background = '#4caf50'; } else { icon.className = 'v-icon notranslate mdi mdi-check-circle theme--dark'; icon.style.color = '#4caf50'; } const message = 'Re-indexed: ' + result.page_title + '\n\n' + '✓ Vectors: ' + result.vector_chunks_created + ' chunks\n' + '✓ Entities: ' + result.graph_entities_extracted + '\n' + '✓ Relationships: ' + result.graph_relationships_created + '\n' + '⏱ Time: ' + Math.round(result.processing_time_ms) + 'ms'; log('Success:', message); // Show notification showNotification('success', 'Page Re-indexed', message); // Reset button setTimeout(function() { if (isFloating) { button.innerHTML = originalContent; button.style.background = '#1976d2'; } else { icon.className = originalIcon; icon.style.color = ''; } button.disabled = false; }, 3000); } else { throw new Error(result.detail || result.error || 'Re-indexing failed'); } } catch (error) { console.error('[Library Desk] Error:', error); if (isFloating) { button.innerHTML = '✗ Failed'; button.style.background = '#f44336'; } else { icon.className = 'v-icon notranslate mdi mdi-alert-circle theme--dark'; icon.style.color = '#f44336'; } showNotification('error', 'Re-indexing Failed', error.message + '\n\nCheck browser console for details.'); // Reset button setTimeout(function() { if (isFloating) { button.innerHTML = originalContent; button.style.background = '#1976d2'; } else { icon.className = originalIcon; icon.style.color = ''; } button.disabled = false; }, 4000); } } // ============================================================================ // ENTITY LINKING FUNCTIONALITY // ============================================================================ async function linkEntities(pagePath, button) { const isFloating = button.id === 'library-desk-entitylink-btn' && button.innerHTML.includes('Link'); const icon = isFloating ? null : button.querySelector('.v-icon'); const originalContent = isFloating ? button.innerHTML : null; const originalIcon = icon ? icon.className : null; button.disabled = true; if (isFloating) { button.innerHTML = '⏳ Finding...'; button.style.background = '#757575'; } else { icon.className = 'v-icon notranslate mdi mdi-loading mdi-spin theme--dark'; } try { // Get page ID from Wiki.js GraphQL API const pageId = await getPageIdFromPath(pagePath); if (!pageId) { throw new Error('Could not get page ID from Wiki.js'); } log('Linking entities for page', pageId, '...'); // Call entity linking endpoint const response = await fetch(CONFIG.libraryDeskUrl + '/entity-linking/link-page', { method: 'POST', headers: { 'Authorization': 'Bearer ' + CONFIG.apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ user: CONFIG.user, page_id: pageId, create_relationships: true, re_index_if_changed: true }) }); const result = await response.json(); if (response.ok) { if (isFloating) { button.innerHTML = '✓ Linked!'; button.style.background = '#4caf50'; } else { icon.className = 'v-icon notranslate mdi mdi-check-circle theme--dark'; icon.style.color = '#4caf50'; } const entityList = result.entities_found .slice(0, 5) .map(e => `- ${e.name} (${e.mentions} mentions)${e.path ? ' ✓ linked' : ''}`) .join('\n'); const message = 'Entity Linking Complete!\n\n' + 'Found: ' + result.entities_found.length + ' entities\n' + 'Wiki links added: ' + result.content_links_added + '\n' + 'Graph links added: ' + result.new_graph_links_created + '\n' + 'Content updated: ' + (result.content_updated ? 'Yes' : 'No') + '\n' + 'Re-indexed: ' + (result.re_indexed ? 'Yes' : 'No') + '\n\n' + 'Top entities:\n' + (entityList || 'None'); log('Success:', result); alert(message); // Reload page if content was updated if (result.content_updated) { log('Reloading page to show updated content...'); setTimeout(function() { window.location.reload(); }, 1500); } else { setTimeout(function() { if (isFloating) { button.innerHTML = originalContent; button.style.background = '#43a047'; } else { icon.className = originalIcon; icon.style.color = ''; } button.disabled = false; }, 3000); } } else { throw new Error(result.detail || 'Entity linking failed'); } } catch (error) { console.error('[Library Desk] Error:', error); if (isFloating) { button.innerHTML = '✗ Failed'; button.style.background = '#f44336'; } else { icon.className = 'v-icon notranslate mdi mdi-alert-circle theme--dark'; icon.style.color = '#f44336'; } alert('Entity linking failed:\n\n' + error.message); setTimeout(function() { if (isFloating) { button.innerHTML = originalContent; button.style.background = '#43a047'; } else { icon.className = originalIcon; icon.style.color = ''; } button.disabled = false; }, 4000); } } // ============================================================================ // SHARED UTILITIES // ============================================================================ async function getPageIdFromPath(pagePath) { /** * Query Wiki.js GraphQL API to get page ID from path. * Uses singleByPath query which requires path and locale. */ try { // Get locale from URL or default to 'en' const urlLocaleMatch = window.location.pathname.match(/^\/([a-z]{2})\//); const locale = urlLocaleMatch ? urlLocaleMatch[1] : 'en'; log('Querying Wiki.js GraphQL for page:', pagePath, 'locale:', locale); const query = ` query ($path: String!, $locale: String!) { pages { singleByPath(path: $path, locale: $locale) { id } } } `; const response = await fetch('/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ query: query, variables: { path: pagePath, locale: locale } }) }); const result = await response.json(); log('GraphQL response:', result); if (result.data && result.data.pages && result.data.pages.singleByPath) { const pageId = result.data.pages.singleByPath.id; log('Found page ID from GraphQL:', pageId); return parseInt(pageId, 10); } if (result.errors) { log('GraphQL errors:', result.errors.map(e => e.message).join(', ')); } log('GraphQL query returned no page'); return null; } catch (error) { log('GraphQL query failed:', error.message); return null; } } function getPageId() { /** * Synchronous wrapper - returns null and triggers async fetch. * Actual buttons will need to call getPageIdFromPath() directly. */ log('getPageId() called - page ID must be fetched asynchronously via getPageIdFromPath()'); return null; } function waitForPageElement(callback, maxAttempts = 10, interval = 500) { /** * Wait for Vue store to be available, then call callback. */ let attempts = 0; function check() { attempts++; // Check if Vue store or page data is available const hasVueStore = window.$store && (window.$store.state || window.$store.get); const hasPageData = window.$page || window.pageId; if (hasVueStore || hasPageData) { log('Vue/page data available after', attempts, 'attempts'); callback(); } else if (attempts < maxAttempts) { log('Waiting for Vue/page data... attempt', attempts); setTimeout(check, interval); } else { log('Vue/page data never appeared after', maxAttempts, 'attempts - adding buttons anyway'); callback(); // Add buttons anyway, they just won't work } } check(); } function findToolbar() { const selectors = [ 'header.v-toolbar .v-toolbar__content', '.v-app-bar .v-toolbar__content', 'nav.v-toolbar .v-toolbar__content', 'header .v-toolbar__content', '.v-app-bar__content' ]; for (var i = 0; i < selectors.length; i++) { var elem = document.querySelector(selectors[i]); if (elem) return elem; } return null; } function getPagePath() { var path = window.location.pathname; path = path.replace(/^\//, '').replace(/\/$/, ''); path = path.replace(/^[a-z]{2}\//, ''); // Remove language code // Don't add buttons to home page or special pages if (path === '' || path === 'home' || path.startsWith('_') || path.startsWith('a/')) { return null; } return path; } function showNotification(type, title, message) { // Try Wiki.js notifications if available if (window.$store && typeof window.$store.commit === 'function') { try { window.$store.commit('showNotification', { message: title + ': ' + message, style: type, icon: type === 'success' ? 'check' : 'alert' }); return; } catch (e) { // Fall through to alert } } // Fallback to browser alert alert(title + '\n\n' + message); } log('Library Desk integration loaded successfully'); log('Version: 2.0.0 - Combined re-index and entity linking'); })();