- Add combined integration script with both re-index and entity linking - Add standalone entity linking button - Add standalone re-index button - Auto-detect Library Desk URL from script tag - Support both toolbar and floating button positions - Show real-time status updates and notifications - Auto-reload page after successful entity linking Usage: Inject via Wiki.js Code Injection settings <script src="http://IP:8089/static/wikijs-integration.js"></script>
528 lines
16 KiB
JavaScript
528 lines
16 KiB
JavaScript
/**
|
|
* Library Desk Integration for Wiki.js
|
|
* Combined re-index and entity linking buttons
|
|
*
|
|
* Usage: Add to Wiki.js Code Injection:
|
|
* <script src="http://192.168.86.149:8089/static/wikijs-integration.js"></script>
|
|
*/
|
|
(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 = `<span class="v-btn__content"><i class="v-icon notranslate mdi ${icon} theme--dark" style="font-size: 20px;"></i></span>`;
|
|
|
|
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');
|
|
|
|
})();
|