|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
const infoDiv = document.querySelector('.info'); |
|
const mainDiv = document.querySelector('.main'); |
|
const reportSectionDiv = document.querySelector('.report-section'); |
|
const viewDemoButton = document.getElementById('view-demo-button'); |
|
const backToInfoButton = document.getElementById('back-to-info-button'); |
|
const caseSelectorTabsContainer = document.getElementById('case-selector-tabs-container'); |
|
const reportTextDisplay = document.getElementById('report-text-display'); |
|
const explanationOutput = document.getElementById('explanation-output'); |
|
const explanationContent = document.getElementById('explanation-content'); |
|
const explanationError = document.getElementById('explanation-error'); |
|
const imageContainer = document.getElementById('image-container'); |
|
const reportImage = document.getElementById('report-image'); |
|
const imageLoading = document.getElementById('image-loading'); |
|
const imageError = document.getElementById('image-error'); |
|
const imageModalityHeader = document.getElementById('image-modality-header'); |
|
const ctImageNote = document.getElementById('ct-image-note'); |
|
const appLoading = document.getElementById('app-loading'); |
|
const appError = document.getElementById('app-error'); |
|
|
|
let availableReports = []; |
|
let currentReportName = null; |
|
let currentReportDetails = null; |
|
|
|
let explainAbortController = null; |
|
let reportLoadAbortController = null; |
|
let appLoadingTimeout = null; |
|
let explanationLoadingTimer = null; |
|
|
|
function initialize() { |
|
try { |
|
const reportsDataElement = document.getElementById('reports-data'); |
|
if (reportsDataElement) { |
|
availableReports = JSON.parse(reportsDataElement.textContent); |
|
} else { |
|
displayAppError("Failed to load report list."); |
|
return; |
|
} |
|
} catch (e) { |
|
displayAppError("Failed to parse report list."); |
|
return; |
|
} |
|
|
|
if (availableReports.length === 0) { |
|
displayAppError("No reports available."); |
|
return; |
|
} |
|
|
|
if (viewDemoButton && infoDiv && mainDiv) { |
|
viewDemoButton.addEventListener('click', () => { |
|
infoDiv.style.display = 'none'; |
|
mainDiv.style.display = 'grid'; |
|
if (currentReportName) { |
|
loadReportDetails(currentReportName); |
|
} |
|
}); |
|
} |
|
|
|
if (backToInfoButton && infoDiv && mainDiv) { |
|
backToInfoButton.addEventListener('click', () => { |
|
abortOngoingRequests(); |
|
mainDiv.style.display = 'none'; |
|
infoDiv.style.display = 'flex'; |
|
clearAllOutputs(); |
|
currentReportDetails = null; |
|
reportImage.src = ''; |
|
document.title = "Radiology Report Explainer"; |
|
}); |
|
} |
|
|
|
if (caseSelectorTabsContainer) { |
|
caseSelectorTabsContainer.addEventListener('click', handleCaseSelectionClick); |
|
} |
|
|
|
reportTextDisplay.addEventListener('click', handleSentenceClick); |
|
|
|
const firstCaseButton = caseSelectorTabsContainer?.querySelector('.nav-button-case'); |
|
if (firstCaseButton) { |
|
currentReportName = firstCaseButton.dataset.reportName; |
|
setActiveCaseButton(firstCaseButton); |
|
loadReportDetails(currentReportName); |
|
} else { |
|
displayAppError("No cases found to load initially."); |
|
} |
|
} |
|
|
|
function handleCaseSelectionClick(event) { |
|
const clickedButton = event.target.closest('.nav-button-case'); |
|
if (!clickedButton) return; |
|
|
|
const selectedName = clickedButton.dataset.reportName; |
|
if (selectedName && selectedName !== currentReportName) { |
|
abortOngoingRequests(); |
|
currentReportName = selectedName; |
|
setActiveCaseButton(clickedButton); |
|
loadReportDetails(currentReportName); |
|
} |
|
} |
|
|
|
async function handleSentenceClick(event) { |
|
const clickedElement = event.target; |
|
if (!clickedElement.classList.contains('report-sentence') || clickedElement.tagName !== 'SPAN') return; |
|
|
|
const sentenceText = clickedElement.dataset.sentence; |
|
if (!sentenceText || !currentReportName) return; |
|
|
|
abortOngoingRequests(['report']); |
|
explainAbortController = new AbortController(); |
|
|
|
document.querySelectorAll('#report-text-display .selected-sentence').forEach(el => el.classList.remove('selected-sentence')); |
|
clickedElement.classList.add('selected-sentence'); |
|
|
|
adjustExplanationPosition(clickedElement); |
|
|
|
try { |
|
await Promise.all([ |
|
fetchExplanation(sentenceText, explainAbortController.signal), |
|
]); |
|
} catch (error) { |
|
if (error.name !== 'AbortError') { |
|
console.error("Error during sentence processing:", error); |
|
} |
|
} |
|
} |
|
|
|
async function loadReportDetails(reportName) { |
|
abortOngoingRequests(); |
|
reportLoadAbortController = new AbortController(); |
|
const signal = reportLoadAbortController.signal; |
|
|
|
setLoadingState(true, 'report'); |
|
clearAllOutputs(true); |
|
|
|
try { |
|
const response = await fetch(`/get_report_details/${encodeURIComponent(reportName)}`, { signal }); |
|
if (signal.aborted) return; |
|
|
|
if (!response.ok) { |
|
const errorData = await response.json().catch(() => ({ error: `HTTP error ${response.status}` })); |
|
throw new Error(errorData.error || `HTTP error ${response.status}`); |
|
} |
|
|
|
currentReportDetails = await response.json(); |
|
if (signal.aborted) return; |
|
|
|
document.title = `${reportName} - Radiology Explainer`; |
|
|
|
|
|
if (imageModalityHeader && currentReportDetails.image_type) { |
|
imageModalityHeader.textContent = currentReportDetails.image_type; |
|
} |
|
|
|
|
|
if (ctImageNote) { |
|
if (currentReportDetails.image_type === 'CT' && currentReportDetails.image_file) { |
|
ctImageNote.style.display = 'block'; |
|
} else { |
|
ctImageNote.style.display = 'none'; |
|
} |
|
} |
|
|
|
|
|
if (currentReportDetails.image_file) { |
|
const imageUrl = `${currentReportDetails.image_file}`; |
|
|
|
reportImage.onload = null; |
|
reportImage.onerror = null; |
|
|
|
reportImage.onload = () => { |
|
imageLoading.style.display = 'none'; |
|
reportImage.style.display = 'block'; |
|
imageError.style.display = 'none'; |
|
}; |
|
reportImage.onerror = () => { |
|
imageLoading.style.display = 'none'; |
|
reportImage.style.display = 'none'; |
|
displayImageError("Failed to load image file."); |
|
}; |
|
|
|
reportImage.src = imageUrl; |
|
reportImage.alt = `Radiology Image for ${reportName}`; |
|
} else { |
|
displayImageError("Image path not configured for this report."); |
|
if (ctImageNote) ctImageNote.style.display = 'none'; |
|
} |
|
|
|
renderReportTextWithLineBreaks(currentReportDetails.text || ''); |
|
} catch (error) { |
|
if (error.name !== 'AbortError') { |
|
displayReportTextError(`Failed to load report: ${error.message}`); |
|
|
|
if (reportImage) { |
|
reportImage.style.display = 'none'; |
|
reportImage.src = ''; |
|
reportImage.onload = null; |
|
reportImage.onerror = null; |
|
} |
|
if (imageLoading) imageLoading.style.display = 'none'; |
|
if (imageError) imageError.style.display = 'none'; |
|
if (ctImageNote) ctImageNote.style.display = 'none'; |
|
clearExplanationAndLocationUI(); |
|
} |
|
} finally { |
|
if (reportLoadAbortController?.signal === signal) { |
|
reportLoadAbortController = null; |
|
} |
|
if (!signal.aborted) { |
|
setLoadingState(false, 'report'); |
|
} |
|
} |
|
} |
|
|
|
function renderReportTextWithLineBreaks(text) { |
|
reportTextDisplay.innerHTML = ''; |
|
reportTextDisplay.classList.remove('loading', 'error'); |
|
|
|
const lines = text.split('\n'); |
|
if (lines.length === 0 || (lines.length === 1 && !lines[0].trim())) { |
|
reportTextDisplay.textContent = 'Report text is empty or could not be processed.'; |
|
return; |
|
} |
|
|
|
lines.forEach((line, index) => { |
|
const trimmedLine = line.trim(); |
|
if (trimmedLine !== '') { |
|
const sentences = splitSentences(trimmedLine); |
|
sentences.forEach(sentence => { |
|
if (sentence) { |
|
const span = document.createElement('span'); |
|
span.textContent = sentence + ' '; |
|
if (!sentence.includes('Image source: ') && sentence.includes(' ') || !sentence.includes(':')) { |
|
span.classList.add('report-sentence'); |
|
} |
|
span.dataset.sentence = sentence; |
|
reportTextDisplay.appendChild(span); |
|
} |
|
}); |
|
} |
|
if (index < lines.length - 1) { |
|
reportTextDisplay.appendChild(document.createElement('br')); |
|
} |
|
}); |
|
} |
|
|
|
async function fetchExplanation(sentence, signal) { |
|
explanationError.style.display = 'none'; |
|
if (!currentReportName) { |
|
displayExplanationError("No report selected."); |
|
return; |
|
} |
|
|
|
if (explanationLoadingTimer) { |
|
clearTimeout(explanationLoadingTimer); |
|
} |
|
|
|
explanationLoadingTimer = setTimeout(() => { |
|
if (!signal.aborted) { |
|
explanationOutput.classList.add('loading'); |
|
explanationContent.textContent = ''; |
|
} |
|
explanationLoadingTimer = null; |
|
}, 150); |
|
|
|
try { |
|
const response = await fetch('/explain', { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify({ sentence, report_name: currentReportName }), |
|
signal |
|
}); |
|
|
|
if (signal.aborted) return; |
|
|
|
if (!response.ok) { |
|
if (explanationLoadingTimer) clearTimeout(explanationLoadingTimer); |
|
explanationLoadingTimer = null; |
|
explanationOutput.classList.remove('loading'); |
|
const errorData = await response.json().catch(() => ({ error: `HTTP error ${response.status}` })); |
|
throw new Error(errorData.error || `HTTP error ${response.status}`); |
|
} |
|
|
|
const data = await response.json(); |
|
if (signal.aborted) return; |
|
|
|
if (explanationLoadingTimer) clearTimeout(explanationLoadingTimer); |
|
explanationLoadingTimer = null; |
|
explanationOutput.classList.remove('loading'); |
|
requestAnimationFrame(() => { |
|
explanationContent.textContent = data.explanation || "No explanation content received."; |
|
adjustExplanationPosition(); |
|
}); |
|
} catch (error) { |
|
if (explanationLoadingTimer) clearTimeout(explanationLoadingTimer); |
|
explanationLoadingTimer = null; |
|
explanationOutput.classList.remove('loading'); |
|
if (error.name !== 'AbortError') { |
|
displayExplanationError(`Explanation Error: ${error.message}`); |
|
} |
|
} |
|
} |
|
|
|
function setLoadingState(isLoading, type = 'all') { |
|
if (type === 'all' || type === 'report') { |
|
if (isLoading) { |
|
if (appLoadingTimeout) { |
|
clearTimeout(appLoadingTimeout); |
|
appLoadingTimeout = null; |
|
} |
|
|
|
if (appError) appError.style.display = 'none'; |
|
if (reportImage) reportImage.style.display = 'none'; |
|
if (imageError) imageError.style.display = 'none'; |
|
if (ctImageNote) ctImageNote.style.display = 'none'; |
|
clearExplanationAndLocationUI(); |
|
|
|
appLoadingTimeout = setTimeout(() => { |
|
if (appLoading) appLoading.style.display = 'block'; |
|
|
|
|
|
if (reportTextDisplay) { |
|
reportTextDisplay.innerHTML = 'Loading...'; |
|
reportTextDisplay.classList.add('loading'); |
|
reportTextDisplay.classList.remove('error'); |
|
} |
|
if (imageLoading) imageLoading.style.display = 'block'; |
|
}, 200); |
|
|
|
} else { |
|
if (appLoadingTimeout) { |
|
clearTimeout(appLoadingTimeout); |
|
appLoadingTimeout = null; |
|
} |
|
if (appLoading) appLoading.style.display = 'none'; |
|
} |
|
} |
|
} |
|
function clearAllOutputs(keepReportTextLoading = false) { |
|
if (!keepReportTextLoading && reportTextDisplay) { |
|
reportTextDisplay.innerHTML = 'Select a report to view its text.'; |
|
reportTextDisplay.classList.remove('loading', 'error'); |
|
} |
|
if (reportImage) { |
|
reportImage.style.display = 'none'; |
|
reportImage.onload = null; |
|
reportImage.onerror = null; |
|
reportImage.src = ''; |
|
} |
|
if (imageError) imageError.style.display = 'none'; |
|
if (ctImageNote) ctImageNote.style.display = 'none'; |
|
clearExplanationAndLocationUI(); |
|
if (appError) appError.style.display = 'none'; |
|
|
|
if (imageModalityHeader) { |
|
imageModalityHeader.textContent = 'Medical Image'; |
|
} |
|
} |
|
|
|
function clearExplanationAndLocationUI() { |
|
if (explanationContent) { |
|
explanationContent.textContent = 'Click a sentence to see the explanation here.'; |
|
} |
|
if (explanationLoadingTimer) { |
|
clearTimeout(explanationLoadingTimer); |
|
explanationLoadingTimer = null; |
|
} |
|
explanationOutput.classList.remove('loading'); |
|
explanationError.style.display = 'none'; |
|
document.querySelectorAll('#report-text-display .selected-sentence').forEach(el => el.classList.remove('selected-sentence')); |
|
} |
|
|
|
function displayReportTextError(message) { |
|
reportTextDisplay.innerHTML = `<span class="error-message">${message}</span>`; |
|
reportTextDisplay.classList.add('error'); |
|
reportTextDisplay.classList.remove('loading'); |
|
} |
|
|
|
function displayAppError(message) { |
|
appError.textContent = `Error: ${message}`; |
|
appError.style.display = 'block'; |
|
appLoading.style.display = 'none'; |
|
} |
|
|
|
function displayImageError(message) { |
|
imageError.textContent = message; |
|
imageError.style.display = 'block'; |
|
imageLoading.style.display = 'none'; |
|
reportImage.style.display = 'none'; |
|
if (ctImageNote) ctImageNote.style.display = 'none'; |
|
} |
|
|
|
function displayExplanationError(message) { |
|
explanationError.textContent = message; |
|
explanationError.style.display = 'block'; |
|
explanationOutput.classList.remove('loading'); |
|
if (explanationContent) explanationContent.textContent = ''; |
|
} |
|
|
|
function setActiveCaseButton(activeButton) { |
|
if (!caseSelectorTabsContainer) return; |
|
caseSelectorTabsContainer.querySelectorAll('.nav-button-case').forEach(btn => btn.classList.remove('active')); |
|
if (activeButton) activeButton.classList.add('active'); |
|
} |
|
|
|
function splitSentences(text) { |
|
if (!text) return []; |
|
try { |
|
if (typeof nlp !== 'function') { |
|
const basicSentences = text.match(/[^.?!]+[.?!]['"]?(\s+|$)/g); |
|
return basicSentences ? basicSentences.map(s => s.trim()).filter(s => s.length > 0) : []; |
|
} |
|
const doc = nlp(text); |
|
return doc.sentences().out('array').map(s => s.trim()).filter(s => s.length > 0); |
|
} catch (e) { |
|
const basicSentences = text.match(/[^.?!]+[.?!]['"]?(\s+|$)/g); |
|
return basicSentences ? basicSentences.map(s => s.trim()).filter(s => s.length > 0) : []; |
|
} |
|
} |
|
|
|
function adjustExplanationPosition(clickedSentenceElement) { |
|
const targetSentenceElement = clickedSentenceElement || document.querySelector('#report-text-display .selected-sentence'); |
|
if (!targetSentenceElement) return; |
|
|
|
const explanationSection = explanationOutput.closest('.explanation-section'); |
|
|
|
if (explanationOutput && explanationSection && reportSectionDiv) { |
|
const sentenceRect = targetSentenceElement.getBoundingClientRect(); |
|
const explanationSectionRect = explanationSection.getBoundingClientRect(); |
|
|
|
|
|
const explanationHeight = explanationOutput.offsetHeight || 200; |
|
|
|
|
|
let newTop = sentenceRect.top - explanationSectionRect.top; |
|
|
|
|
|
const explanationBoxAbsoluteBottom = explanationSectionRect.top + newTop + explanationHeight + 15; |
|
|
|
const viewportHeight = window.innerHeight; |
|
const pageBottomOverflow = explanationBoxAbsoluteBottom - viewportHeight; |
|
|
|
if (pageBottomOverflow > 0) { |
|
|
|
newTop -= pageBottomOverflow; |
|
} |
|
|
|
|
|
newTop = Math.max(0, newTop); |
|
|
|
explanationOutput.style.top = `${newTop}px`; |
|
} |
|
} |
|
|
|
function abortOngoingRequests(excludeTypes = []) { |
|
if (!excludeTypes.includes('report') && reportLoadAbortController) { |
|
reportLoadAbortController.abort(); |
|
reportLoadAbortController = null; |
|
} |
|
if (!excludeTypes.includes('explain') && explainAbortController) { |
|
explainAbortController.abort(); |
|
explainAbortController = null; |
|
} |
|
} |
|
|
|
initialize(); |
|
}); |
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
|
|
|
|
|
|
const infoButton = document.getElementById('info-button'); |
|
const immersiveDialogOverlay = document.getElementById('immersive-info-dialog'); |
|
const dialogCloseButton = document.getElementById('dialog-close-button'); |
|
|
|
if (infoButton && immersiveDialogOverlay && dialogCloseButton) { |
|
const openDialog = () => { |
|
immersiveDialogOverlay.style.display = 'flex'; |
|
|
|
setTimeout(() => { |
|
immersiveDialogOverlay.classList.add('active'); |
|
}, 10); |
|
document.body.style.overflow = 'hidden'; |
|
}; |
|
|
|
const closeDialog = () => { |
|
immersiveDialogOverlay.classList.remove('active'); |
|
|
|
setTimeout(() => { |
|
immersiveDialogOverlay.style.display = 'none'; |
|
}, 300); |
|
document.body.style.overflow = ''; |
|
}; |
|
|
|
infoButton.addEventListener('click', openDialog); |
|
dialogCloseButton.addEventListener('click', closeDialog); |
|
|
|
|
|
immersiveDialogOverlay.addEventListener('click', (event) => { |
|
if (event.target === immersiveDialogOverlay) { |
|
closeDialog(); |
|
} |
|
}); |
|
|
|
|
|
document.addEventListener('keydown', (event) => { |
|
if (event.key === 'Escape' && immersiveDialogOverlay.classList.contains('active')) { |
|
closeDialog(); |
|
} |
|
}); |
|
} else { |
|
|
|
if (!infoButton) console.error('Dialog trigger button (#info-button) not found.'); |
|
if (!immersiveDialogOverlay) console.error('Immersive dialog (#immersive-info-dialog) not found.'); |
|
if (!dialogCloseButton) console.error('Dialog close button (#dialog-close-button) not found.'); |
|
} |
|
|
|
|
|
}); |
|
|