Files
portainer-core/services/library-desk/static/wikijs-integration.js
T
jpmschweitzer d80f063186 feat(library-desk): add Wiki.js integration buttons for entity linking
- 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>
2025-12-10 01:16:46 +01:00

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');
})();