| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| (function() { |
| 'use strict'; |
|
|
| |
| const EntityLinking = { |
| initialized: false, |
| configuredKBs: [], |
| currentSpanId: null, |
| currentInstanceId: null, |
| searchTimeout: null, |
| modal: null, |
| config: null, |
| |
| currentLinkedEntities: [], |
| multiSelect: false |
| }; |
|
|
| |
| |
| |
| function init() { |
| if (EntityLinking.initialized) { |
| console.log('[EntityLinking] Already initialized'); |
| return; |
| } |
|
|
| console.log('[EntityLinking] Initializing...'); |
|
|
| |
| const schemas = document.querySelectorAll('[data-entity-linking]'); |
| if (schemas.length === 0) { |
| console.log('[EntityLinking] No schemas with entity linking enabled'); |
| return; |
| } |
|
|
| |
| try { |
| const configStr = schemas[0].getAttribute('data-entity-linking'); |
| EntityLinking.config = JSON.parse(configStr); |
| EntityLinking.multiSelect = EntityLinking.config.multi_select || false; |
| console.log('[EntityLinking] Config:', EntityLinking.config); |
| console.log('[EntityLinking] Multi-select enabled:', EntityLinking.multiSelect); |
| } catch (e) { |
| console.error('[EntityLinking] Failed to parse config:', e); |
| return; |
| } |
|
|
| |
| createSearchModal(); |
|
|
| |
| fetchConfiguredKBs(); |
|
|
| |
| setupEventListeners(); |
|
|
| EntityLinking.initialized = true; |
| console.log('[EntityLinking] Initialized successfully'); |
| } |
|
|
| |
| |
| |
| async function fetchConfiguredKBs() { |
| try { |
| const response = await fetch('/api/entity_linking/configured_kbs'); |
| if (response.ok) { |
| const data = await response.json(); |
| EntityLinking.configuredKBs = data.knowledge_bases || []; |
| console.log('[EntityLinking] Configured KBs:', EntityLinking.configuredKBs); |
|
|
| |
| updateKBSelector(); |
| } |
| } catch (e) { |
| console.error('[EntityLinking] Failed to fetch configured KBs:', e); |
| } |
| } |
|
|
| |
| |
| |
| function updateKBSelector() { |
| const selector = document.getElementById('el-kb-selector'); |
| if (!selector) return; |
|
|
| selector.innerHTML = ''; |
|
|
| EntityLinking.configuredKBs.forEach(kb => { |
| const option = document.createElement('option'); |
| option.value = kb.name; |
| option.textContent = `${kb.name} (${kb.type})`; |
| selector.appendChild(option); |
| }); |
| } |
|
|
| |
| |
| |
| function createSearchModal() { |
| |
| if (document.getElementById('entity-linking-modal')) { |
| EntityLinking.modal = document.getElementById('entity-linking-modal'); |
| return; |
| } |
|
|
| const modal = document.createElement('div'); |
| modal.id = 'entity-linking-modal'; |
| modal.className = 'el-modal'; |
| modal.innerHTML = ` |
| <div class="el-modal-content"> |
| <div class="el-modal-header"> |
| <h3>Link to Knowledge Base Entity</h3> |
| <button class="el-close-btn" title="Close">×</button> |
| </div> |
| <div class="el-modal-body"> |
| <div class="el-span-info"> |
| <strong>Selected text:</strong> |
| <span id="el-selected-text" class="el-selected-text"></span> |
| </div> |
| <div class="el-search-container"> |
| <select id="el-kb-selector" class="el-kb-selector"> |
| <option value="">Select Knowledge Base...</option> |
| </select> |
| <input type="text" id="el-search-input" class="el-search-input" |
| placeholder="Search for entity..."> |
| <button id="el-search-btn" class="el-search-btn">Search</button> |
| </div> |
| <div id="el-loading" class="el-loading" style="display: none;"> |
| <span class="el-spinner"></span> Searching... |
| </div> |
| <div id="el-results" class="el-results"></div> |
| <div id="el-current-link" class="el-current-link" style="display: none;"> |
| <strong>Currently linked to:</strong> |
| <div id="el-current-entity"></div> |
| <button id="el-remove-link" class="el-remove-link-btn">Remove Link</button> |
| </div> |
| </div> |
| <div class="el-modal-footer"> |
| <button id="el-save-btn" class="el-save-btn" style="display: none;">Save Selection</button> |
| <button id="el-cancel-btn" class="el-cancel-btn">Cancel</button> |
| </div> |
| </div> |
| `; |
|
|
| document.body.appendChild(modal); |
| EntityLinking.modal = modal; |
|
|
| |
| modal.querySelector('.el-close-btn').addEventListener('click', closeModal); |
| modal.querySelector('#el-cancel-btn').addEventListener('click', closeModal); |
| modal.querySelector('#el-save-btn').addEventListener('click', saveMultiSelect); |
| modal.querySelector('#el-search-btn').addEventListener('click', performSearch); |
| modal.querySelector('#el-search-input').addEventListener('keypress', (e) => { |
| if (e.key === 'Enter') { |
| performSearch(); |
| } |
| }); |
| modal.querySelector('#el-search-input').addEventListener('input', debounceSearch); |
| modal.querySelector('#el-remove-link').addEventListener('click', removeCurrentLink); |
|
|
| |
| modal.addEventListener('click', (e) => { |
| if (e.target === modal) { |
| closeModal(); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| function setupEventListeners() { |
| |
| document.addEventListener('spanCreated', handleSpanCreated); |
|
|
| |
| addLinkIconsToSpans(); |
|
|
| |
| document.addEventListener('mouseover', handleSpanHover); |
| document.addEventListener('mouseout', handleSpanHoverOut); |
| } |
|
|
| |
| |
| |
| function getSchemasWithEntityLinking() { |
| const schemas = new Set(); |
| document.querySelectorAll('[data-entity-linking]').forEach(el => { |
| |
| |
| const schemaName = el.getAttribute('data-schema-name') || |
| el.querySelector('[name^="span_label:::"]')?.name?.split(':::')[1] || |
| el.id?.replace('annotation-form-', ''); |
| if (schemaName) { |
| schemas.add(schemaName); |
| } |
| }); |
| return schemas; |
| } |
|
|
| |
| |
| |
| function addLinkIconsToSpans() { |
| const enabledSchemas = getSchemasWithEntityLinking(); |
| if (enabledSchemas.size === 0) { |
| return; |
| } |
|
|
| |
| const overlays = document.querySelectorAll('.span-overlay-pure'); |
| overlays.forEach(overlay => { |
| |
| const spanSchema = overlay.dataset.schema; |
| if (spanSchema && enabledSchemas.has(spanSchema) && !overlay.querySelector('.el-link-icon')) { |
| addLinkIconToSpan(overlay); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| function addLinkIconToSpan(overlay) { |
| |
| const enabledSchemas = getSchemasWithEntityLinking(); |
| const spanSchema = overlay.dataset.schema; |
| if (spanSchema && !enabledSchemas.has(spanSchema)) { |
| return; |
| } |
|
|
| |
| const controlsContainer = overlay.querySelector('.span-controls'); |
| if (!controlsContainer) { |
| console.debug('[EntityLinking] No controls container found for overlay'); |
| return; |
| } |
|
|
| const icon = document.createElement('button'); |
| icon.className = 'el-link-icon'; |
| icon.type = 'button'; |
| icon.innerHTML = overlay.classList.contains('has-entity-link') ? |
| '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>' : |
| '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>'; |
| icon.title = overlay.classList.contains('has-entity-link') ? |
| 'Edit entity link' : 'Link to knowledge base'; |
|
|
| |
| icon.style.backgroundColor = 'rgba(99, 102, 241, 0.9)'; |
| icon.style.color = 'white'; |
| icon.style.border = 'none'; |
| icon.style.borderRadius = '50%'; |
| icon.style.width = '16px'; |
| icon.style.height = '16px'; |
| icon.style.minWidth = '16px'; |
| icon.style.minHeight = '16px'; |
| icon.style.padding = '0'; |
| icon.style.margin = '0'; |
| icon.style.cursor = 'pointer'; |
| icon.style.display = 'flex'; |
| icon.style.alignItems = 'center'; |
| icon.style.justifyContent = 'center'; |
| icon.style.flexShrink = '0'; |
|
|
| icon.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| openModal(overlay); |
| }); |
|
|
| |
| const deleteBtn = controlsContainer.querySelector('.span-delete-btn'); |
| if (deleteBtn) { |
| controlsContainer.insertBefore(icon, deleteBtn); |
| } else { |
| controlsContainer.appendChild(icon); |
| } |
| } |
|
|
| |
| |
| |
| function handleSpanCreated(event) { |
| const span = event.detail.span; |
| const spanElement = event.detail.element; |
|
|
| if (!spanElement) return; |
|
|
| |
| addLinkIconToSpan(spanElement); |
|
|
| |
| if (EntityLinking.config && EntityLinking.config.auto_search) { |
| openModal(spanElement); |
| } |
| } |
|
|
| |
| |
| |
| function handleSpanHover(event) { |
| const span = event.target.closest('.span-highlight.has-entity-link'); |
| if (!span) return; |
|
|
| const kbId = span.getAttribute('data-kb-id'); |
| const kbSource = span.getAttribute('data-kb-source'); |
| const kbLabel = span.getAttribute('data-kb-label'); |
|
|
| if (!kbId || !kbSource) return; |
|
|
| |
| showEntityTooltip(span, kbId, kbSource, kbLabel); |
| } |
|
|
| |
| |
| |
| function handleSpanHoverOut(event) { |
| const span = event.target.closest('.span-highlight.has-entity-link'); |
| if (!span) return; |
|
|
| hideEntityTooltip(); |
| } |
|
|
| |
| |
| |
| function showEntityTooltip(span, kbId, kbSource, kbLabel) { |
| |
| hideEntityTooltip(); |
|
|
| const tooltip = document.createElement('div'); |
| tooltip.id = 'el-entity-tooltip'; |
| tooltip.className = 'el-entity-tooltip'; |
| tooltip.innerHTML = ` |
| <div class="el-tooltip-header"> |
| <span class="el-tooltip-kb">${escapeHtml(kbSource)}</span> |
| <span class="el-tooltip-id">${escapeHtml(kbId)}</span> |
| </div> |
| <div class="el-tooltip-label">${escapeHtml(kbLabel || 'Loading...')}</div> |
| `; |
|
|
| |
| const rect = span.getBoundingClientRect(); |
| tooltip.style.position = 'fixed'; |
| tooltip.style.left = `${rect.left}px`; |
| tooltip.style.top = `${rect.bottom + 5}px`; |
|
|
| document.body.appendChild(tooltip); |
|
|
| |
| if (!kbLabel) { |
| fetchEntityDetails(kbId, kbSource, tooltip); |
| } |
| } |
|
|
| |
| |
| |
| function hideEntityTooltip() { |
| const tooltip = document.getElementById('el-entity-tooltip'); |
| if (tooltip) { |
| tooltip.remove(); |
| } |
| } |
|
|
| |
| |
| |
| async function fetchEntityDetails(kbId, kbSource, tooltip) { |
| try { |
| const response = await fetch(`/api/entity_linking/entity/${kbSource}/${kbId}`); |
| if (response.ok) { |
| const data = await response.json(); |
| const entity = data.entity; |
|
|
| if (tooltip && document.body.contains(tooltip)) { |
| tooltip.querySelector('.el-tooltip-label').textContent = entity.label; |
| if (entity.description) { |
| const desc = document.createElement('div'); |
| desc.className = 'el-tooltip-desc'; |
| desc.textContent = entity.description; |
| tooltip.appendChild(desc); |
| } |
| } |
| } |
| } catch (e) { |
| console.error('[EntityLinking] Failed to fetch entity details:', e); |
| } |
| } |
|
|
| |
| |
| |
| function openModal(spanElement) { |
| if (!EntityLinking.modal) return; |
|
|
| |
| EntityLinking.currentSpanId = spanElement.getAttribute('data-annotation-id'); |
| EntityLinking.currentInstanceId = document.getElementById('instance_id')?.value; |
|
|
| |
| const start = parseInt(spanElement.getAttribute('data-start'), 10); |
| const end = parseInt(spanElement.getAttribute('data-end'), 10); |
| let selectedText = ''; |
|
|
| |
| const textContent = document.getElementById('text-content'); |
| if (textContent && !isNaN(start) && !isNaN(end)) { |
| const originalText = textContent.getAttribute('data-original-text') || textContent.textContent; |
| selectedText = originalText.substring(start, end); |
| } |
|
|
| |
| if (!selectedText) { |
| const label = spanElement.querySelector('.span-label'); |
| selectedText = label ? label.textContent : spanElement.getAttribute('data-label') || ''; |
| } |
|
|
| document.getElementById('el-selected-text').textContent = selectedText; |
| document.getElementById('el-search-input').value = selectedText; |
|
|
| |
| const kbId = spanElement.getAttribute('data-kb-id'); |
| const kbSource = spanElement.getAttribute('data-kb-source'); |
| const kbLabel = spanElement.getAttribute('data-kb-label'); |
|
|
| |
| EntityLinking.currentLinkedEntities = []; |
|
|
| if (kbId && kbSource) { |
| |
| |
| EntityLinking.currentLinkedEntities.push({ |
| kb_id: kbId, |
| kb_source: kbSource, |
| kb_label: kbLabel |
| }); |
| showCurrentLink(kbId, kbSource, kbLabel); |
| } else { |
| document.getElementById('el-current-link').style.display = 'none'; |
| } |
|
|
| |
| document.getElementById('el-results').innerHTML = ''; |
| document.getElementById('el-loading').style.display = 'none'; |
|
|
| |
| const saveBtn = document.getElementById('el-save-btn'); |
| if (saveBtn) { |
| saveBtn.style.display = EntityLinking.multiSelect ? 'inline-block' : 'none'; |
| } |
|
|
| |
| EntityLinking.modal.style.display = 'flex'; |
| document.getElementById('el-search-input').focus(); |
|
|
| |
| if (selectedText && EntityLinking.configuredKBs.length > 0) { |
| const selector = document.getElementById('el-kb-selector'); |
| if (!selector.value && EntityLinking.configuredKBs[0]) { |
| selector.value = EntityLinking.configuredKBs[0].name; |
| } |
| performSearch(); |
| } |
| } |
|
|
| |
| |
| |
| async function saveMultiSelect() { |
| if (!EntityLinking.currentSpanId || !EntityLinking.currentInstanceId) { |
| console.error('[EntityLinking] No span selected for multi-select save'); |
| return; |
| } |
|
|
| |
| const primaryEntity = EntityLinking.currentLinkedEntities[0]; |
|
|
| if (!primaryEntity) { |
| |
| await removeCurrentLink(); |
| return; |
| } |
|
|
| try { |
| const requestBody = { |
| instance_id: EntityLinking.currentInstanceId, |
| span_id: EntityLinking.currentSpanId, |
| kb_id: primaryEntity.kb_id, |
| kb_source: primaryEntity.kb_source, |
| kb_label: primaryEntity.kb_label, |
| |
| linked_entities: EntityLinking.currentLinkedEntities |
| }; |
|
|
| const response = await fetch('/api/entity_linking/update_span', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(requestBody) |
| }); |
|
|
| if (response.ok) { |
| |
| const spanElement = document.querySelector( |
| `.span-overlay-pure[data-annotation-id="${CSS.escape(EntityLinking.currentSpanId)}"]` |
| ); |
|
|
| if (spanElement) { |
| spanElement.setAttribute('data-kb-id', primaryEntity.kb_id); |
| spanElement.setAttribute('data-kb-source', primaryEntity.kb_source); |
| spanElement.setAttribute('data-kb-label', primaryEntity.kb_label || ''); |
| spanElement.classList.add('has-entity-link'); |
|
|
| |
| const icon = spanElement.querySelector('.el-link-icon'); |
| if (icon) { |
| icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>'; |
| icon.title = 'Edit entity link'; |
| } |
| } |
|
|
| closeModal(); |
| console.log('[EntityLinking] Multi-select saved:', EntityLinking.currentLinkedEntities.length, 'entities'); |
| } |
| } catch (e) { |
| console.error('[EntityLinking] Error saving multi-select:', e); |
| } |
| } |
|
|
| |
| |
| |
| function showCurrentLink(kbId, kbSource, kbLabel) { |
| const container = document.getElementById('el-current-link'); |
| const entityDiv = document.getElementById('el-current-entity'); |
|
|
| entityDiv.innerHTML = ` |
| <span class="el-current-kb">${escapeHtml(kbSource)}</span>: |
| <span class="el-current-id">${escapeHtml(kbId)}</span> |
| ${kbLabel ? `<br><span class="el-current-label">${escapeHtml(kbLabel)}</span>` : ''} |
| `; |
|
|
| container.style.display = 'block'; |
| } |
|
|
| |
| |
| |
| function closeModal() { |
| if (EntityLinking.modal) { |
| EntityLinking.modal.style.display = 'none'; |
| } |
| EntityLinking.currentSpanId = null; |
| EntityLinking.currentInstanceId = null; |
| } |
|
|
| |
| |
| |
| function debounceSearch() { |
| clearTimeout(EntityLinking.searchTimeout); |
| EntityLinking.searchTimeout = setTimeout(performSearch, 300); |
| } |
|
|
| |
| |
| |
| |
| async function performSearch() { |
| const query = document.getElementById('el-search-input').value.trim(); |
| const kbName = document.getElementById('el-kb-selector').value; |
|
|
| if (!query || !kbName) { |
| return; |
| } |
|
|
| const loading = document.getElementById('el-loading'); |
| const results = document.getElementById('el-results'); |
|
|
| loading.style.display = 'flex'; |
| results.innerHTML = ''; |
|
|
| try { |
| |
| const words = query.split(/\s+/).filter(w => w.length > 2); |
| const searches = [query]; |
|
|
| |
| if (words.length > 1) { |
| words.forEach(word => { |
| if (!searches.includes(word)) { |
| searches.push(word); |
| } |
| }); |
| } |
|
|
| |
| const searchPromises = searches.slice(0, 3).map(q => |
| fetch(`/api/entity_linking/search?q=${encodeURIComponent(q)}&kb=${encodeURIComponent(kbName)}&limit=5`) |
| .then(r => r.ok ? r.json() : { results: [] }) |
| .catch(() => ({ results: [] })) |
| ); |
|
|
| const searchResults = await Promise.all(searchPromises); |
|
|
| |
| const seenIds = new Set(); |
| const allResults = []; |
|
|
| searchResults.forEach(data => { |
| (data.results || []).forEach(entity => { |
| if (!seenIds.has(entity.entity_id)) { |
| seenIds.add(entity.entity_id); |
| allResults.push(entity); |
| } |
| }); |
| }); |
|
|
| |
| displayResults(allResults.slice(0, 10)); |
| } catch (e) { |
| console.error('[EntityLinking] Search error:', e); |
| results.innerHTML = '<div class="el-error">Search failed. Please try again.</div>'; |
| } finally { |
| loading.style.display = 'none'; |
| } |
| } |
|
|
| |
| |
| |
| function isEntityLinked(entityId, kbSource) { |
| return EntityLinking.currentLinkedEntities.some( |
| e => e.kb_id === entityId && e.kb_source === kbSource |
| ); |
| } |
|
|
| |
| |
| |
| function displayResults(entities) { |
| const results = document.getElementById('el-results'); |
|
|
| if (!entities || entities.length === 0) { |
| results.innerHTML = '<div class="el-no-results">No entities found.</div>'; |
| return; |
| } |
|
|
| const multiSelect = EntityLinking.multiSelect; |
|
|
| results.innerHTML = entities.map(entity => { |
| const isLinked = isEntityLinked(entity.entity_id, entity.kb_source); |
| const linkedClass = isLinked ? 'el-result-item-linked' : ''; |
| const linkedBadge = isLinked ? '<span class="el-linked-badge">✓ Currently Linked</span>' : ''; |
| const checkbox = multiSelect ? |
| `<input type="checkbox" class="el-result-checkbox" ${isLinked ? 'checked' : ''}>` : ''; |
|
|
| return ` |
| <div class="el-result-item ${linkedClass}" data-entity-id="${entity.entity_id}" |
| data-kb-source="${entity.kb_source}" data-label="${escapeHtml(entity.label)}"> |
| <div class="el-result-header"> |
| ${checkbox} |
| <span class="el-result-label">${escapeHtml(entity.label)}</span> |
| <span class="el-result-id">${entity.entity_id}</span> |
| ${linkedBadge} |
| </div> |
| ${entity.description ? `<div class="el-result-desc">${escapeHtml(entity.description)}</div>` : ''} |
| ${entity.aliases && entity.aliases.length > 0 ? |
| `<div class="el-result-aliases">Also: ${entity.aliases.slice(0, 3).map(a => escapeHtml(a)).join(', ')}</div>` : ''} |
| ${entity.url ? `<a href="${entity.url}" target="_blank" class="el-result-link">View in KB</a>` : ''} |
| </div> |
| `; |
| }).join(''); |
|
|
| |
| results.querySelectorAll('.el-result-item').forEach(item => { |
| if (multiSelect) { |
| |
| item.addEventListener('click', (e) => { |
| if (e.target.tagName !== 'A') { |
| const checkbox = item.querySelector('.el-result-checkbox'); |
| if (checkbox && e.target !== checkbox) { |
| checkbox.checked = !checkbox.checked; |
| } |
| updateMultiSelectState(item, checkbox?.checked); |
| } |
| }); |
| } else { |
| |
| item.addEventListener('click', (e) => { |
| if (e.target.tagName !== 'A') { |
| selectEntity(item); |
| } |
| }); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| function updateMultiSelectState(item, isSelected) { |
| const entityId = item.getAttribute('data-entity-id'); |
| const kbSource = item.getAttribute('data-kb-source'); |
| const label = item.getAttribute('data-label'); |
|
|
| if (isSelected) { |
| |
| if (!isEntityLinked(entityId, kbSource)) { |
| EntityLinking.currentLinkedEntities.push({ |
| kb_id: entityId, |
| kb_source: kbSource, |
| kb_label: label |
| }); |
| } |
| item.classList.add('el-result-item-linked'); |
| } else { |
| |
| EntityLinking.currentLinkedEntities = EntityLinking.currentLinkedEntities.filter( |
| e => !(e.kb_id === entityId && e.kb_source === kbSource) |
| ); |
| item.classList.remove('el-result-item-linked'); |
| } |
|
|
| |
| const badge = item.querySelector('.el-linked-badge'); |
| if (isSelected && !badge) { |
| const header = item.querySelector('.el-result-header'); |
| const newBadge = document.createElement('span'); |
| newBadge.className = 'el-linked-badge'; |
| newBadge.textContent = '✓ Currently Linked'; |
| header.appendChild(newBadge); |
| } else if (!isSelected && badge) { |
| badge.remove(); |
| } |
| } |
|
|
| |
| |
| |
| async function selectEntity(item) { |
| const entityId = item.getAttribute('data-entity-id'); |
| const kbSource = item.getAttribute('data-kb-source'); |
| const label = item.getAttribute('data-label'); |
|
|
| console.log('[EntityLinking] selectEntity called with:', { entityId, kbSource, label }); |
| console.log('[EntityLinking] Current state:', { |
| spanId: EntityLinking.currentSpanId, |
| instanceId: EntityLinking.currentInstanceId |
| }); |
|
|
| if (!EntityLinking.currentSpanId || !EntityLinking.currentInstanceId) { |
| console.error('[EntityLinking] No span selected - spanId:', EntityLinking.currentSpanId, 'instanceId:', EntityLinking.currentInstanceId); |
| alert('Error: No span selected. Please try again.'); |
| return; |
| } |
|
|
| try { |
| const requestBody = { |
| instance_id: EntityLinking.currentInstanceId, |
| span_id: EntityLinking.currentSpanId, |
| kb_id: entityId, |
| kb_source: kbSource, |
| kb_label: label |
| }; |
| console.log('[EntityLinking] Sending request:', requestBody); |
|
|
| const response = await fetch('/api/entity_linking/update_span', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify(requestBody) |
| }); |
|
|
| console.log('[EntityLinking] Response status:', response.status); |
|
|
| if (response.ok) { |
| |
| const spanElement = document.querySelector( |
| `.span-overlay-pure[data-annotation-id="${CSS.escape(EntityLinking.currentSpanId)}"]` |
| ); |
|
|
| if (spanElement) { |
| spanElement.setAttribute('data-kb-id', entityId); |
| spanElement.setAttribute('data-kb-source', kbSource); |
| spanElement.setAttribute('data-kb-label', label); |
| spanElement.classList.add('has-entity-link'); |
|
|
| |
| const icon = spanElement.querySelector('.el-link-icon'); |
| if (icon) { |
| icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>'; |
| icon.title = 'Edit entity link'; |
| } |
| } else { |
| console.warn('[EntityLinking] Could not find span element to update'); |
| } |
|
|
| closeModal(); |
| console.log('[EntityLinking] Entity link saved:', entityId); |
| } else { |
| const errorData = await response.json().catch(() => ({})); |
| console.error('[EntityLinking] Failed to save entity link:', errorData); |
| } |
| } catch (e) { |
| console.error('[EntityLinking] Error saving entity link:', e); |
| } |
| } |
|
|
| |
| |
| |
| async function removeCurrentLink() { |
| if (!EntityLinking.currentSpanId || !EntityLinking.currentInstanceId) { |
| return; |
| } |
|
|
| try { |
| const response = await fetch('/api/entity_linking/update_span', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ |
| instance_id: EntityLinking.currentInstanceId, |
| span_id: EntityLinking.currentSpanId, |
| kb_id: null, |
| kb_source: null, |
| kb_label: null |
| }) |
| }); |
|
|
| if (response.ok) { |
| |
| const spanElement = document.querySelector( |
| `.span-overlay-pure[data-annotation-id="${CSS.escape(EntityLinking.currentSpanId)}"]` |
| ); |
|
|
| if (spanElement) { |
| spanElement.removeAttribute('data-kb-id'); |
| spanElement.removeAttribute('data-kb-source'); |
| spanElement.removeAttribute('data-kb-label'); |
| spanElement.classList.remove('has-entity-link'); |
|
|
| |
| const icon = spanElement.querySelector('.el-link-icon'); |
| if (icon) { |
| icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>'; |
| icon.title = 'Link to knowledge base'; |
| } |
| } |
|
|
| document.getElementById('el-current-link').style.display = 'none'; |
| console.log('[EntityLinking] Entity link removed'); |
| } |
| } catch (e) { |
| console.error('[EntityLinking] Error removing entity link:', e); |
| } |
| } |
|
|
| |
| |
| |
| function escapeHtml(text) { |
| if (!text) return ''; |
| const div = document.createElement('div'); |
| div.textContent = text; |
| return div.innerHTML; |
| } |
|
|
| |
| if (document.readyState === 'loading') { |
| document.addEventListener('DOMContentLoaded', init); |
| } else { |
| init(); |
| } |
|
|
| |
| const observer = new MutationObserver((mutations) => { |
| mutations.forEach((mutation) => { |
| mutation.addedNodes.forEach((node) => { |
| if (node.nodeType === Node.ELEMENT_NODE) { |
| |
| if (node.classList && node.classList.contains('span-overlay-pure')) { |
| if (!node.querySelector('.el-link-icon')) { |
| addLinkIconToSpan(node); |
| } |
| } |
| |
| const overlays = node.querySelectorAll && node.querySelectorAll('.span-overlay-pure'); |
| if (overlays) { |
| overlays.forEach(overlay => { |
| if (!overlay.querySelector('.el-link-icon')) { |
| addLinkIconToSpan(overlay); |
| } |
| }); |
| } |
| } |
| }); |
| }); |
| }); |
|
|
| |
| if (document.readyState === 'loading') { |
| document.addEventListener('DOMContentLoaded', () => { |
| observer.observe(document.body, { childList: true, subtree: true }); |
| }); |
| } else { |
| observer.observe(document.body, { childList: true, subtree: true }); |
| } |
|
|
| |
| window.EntityLinking = { |
| init: init, |
| openModal: openModal, |
| closeModal: closeModal, |
| search: performSearch |
| }; |
|
|
| })(); |
|
|