diff --git a/services/library-desk/static/wikijs-integration.js b/services/library-desk/static/wikijs-integration.js new file mode 100644 index 0000000..36b461e --- /dev/null +++ b/services/library-desk/static/wikijs-integration.js @@ -0,0 +1,527 @@ +/** + * 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'); + +})();