/** * 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); } } // 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 Wiki.js to fully render setTimeout(addButtons, 1500); } 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; } 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 { log('Fetching page list...'); const listUrl = CONFIG.libraryDeskUrl + '/wiki/pages?user=' + CONFIG.user + '&limit=200'; const listResponse = await fetch(listUrl, { headers: { 'Authorization': 'Bearer ' + CONFIG.apiKey } }); if (!listResponse.ok) { throw new Error('Failed to fetch page list: ' + listResponse.status); } const pageList = await listResponse.json(); log('Fetched', pageList.pages.length, 'pages'); // Find matching page const page = findMatchingPage(pageList.pages, pagePath); if (!page) { log('Available pages:', pageList.pages.map(p => p.path)); throw new Error('Page not found in Library Desk. Check console for available pages.'); } log('Matched page:', page.id, page.path, page.title); log('Re-indexing...'); // Re-index const response = await fetch(CONFIG.libraryDeskUrl + '/ingest/batch', { method: 'POST', headers: { 'Authorization': 'Bearer ' + CONFIG.apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ user: CONFIG.user, page_ids: [page.id] }) }); const result = await response.json(); if (response.ok && result.successful > 0) { const pageResult = result.results[0]; 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: ' + page.title + '\n\n' + '✓ Vectors: ' + pageResult.vector_chunks_created + ' chunks\n' + '✓ Entities: ' + pageResult.graph_entities_extracted + '\n' + '✓ Relationships: ' + pageResult.graph_relationships_created + '\n' + '⏱ Time: ' + Math.round(pageResult.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 { log('Fetching page list...'); // Step 1: Get page ID const listUrl = CONFIG.libraryDeskUrl + '/wiki/pages?user=' + CONFIG.user + '&limit=200'; const listResponse = await fetch(listUrl, { headers: { 'Authorization': 'Bearer ' + CONFIG.apiKey } }); if (!listResponse.ok) { throw new Error('Failed to fetch page list'); } const pageList = await listResponse.json(); const page = findMatchingPage(pageList.pages, pagePath); if (!page) { throw new Error('Page not found in Library Desk'); } log('Found page:', page.id, page.title); // Step 2: 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: page.id, 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 // ============================================================================ function findMatchingPage(pages, pagePath) { const possiblePaths = [ pagePath, 'users/' + CONFIG.user + '/' + pagePath, pagePath.replace(/^\//, ''), CONFIG.user + '/' + pagePath ]; log('Looking for page matching:', possiblePaths); for (var i = 0; i < pages.length; i++) { var page = pages[i]; for (var j = 0; j < possiblePaths.length; j++) { var testPath = possiblePaths[j]; if (page.path === testPath || page.path.endsWith('/' + testPath) || ('users/' + CONFIG.user + '/' + testPath) === page.path) { return page; } } } return null; } 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'); })();