|
import React from 'react'; |
|
import Card from '@mui/material/Card'; |
|
import CardContent from '@mui/material/CardContent'; |
|
import Typography from '@mui/material/Typography'; |
|
import Link from '@mui/material/Link'; |
|
import './SourcePopup.css'; |
|
|
|
|
|
const getDomainName = (url) => { |
|
try { |
|
if (!url) return 'Unknown Source'; |
|
const hostname = new URL(url).hostname; |
|
const domain = hostname.startsWith('www.') ? hostname.slice(4) : hostname; |
|
const parts = domain.split('.'); |
|
return parts[0].charAt(0).toUpperCase() + parts[0].slice(1); |
|
} catch (err) { |
|
console.error("Error parsing URL for domain name:", url, err); |
|
return 'Invalid URL'; |
|
} |
|
}; |
|
|
|
|
|
function levenshtein(a, b) { |
|
if (a.length === 0) return b.length; |
|
if (b.length === 0) return a.length; |
|
const matrix = []; |
|
for (let i = 0; i <= b.length; i++) matrix[i] = [i]; |
|
for (let j = 0; j <= a.length; j++) matrix[0][j] = j; |
|
for (let i = 1; i <= b.length; i++) { |
|
for (let j = 1; j <= a.length; j++) { |
|
if (b.charAt(i - 1) === a.charAt(j - 1)) { |
|
matrix[i][j] = matrix[i - 1][j - 1]; |
|
} else { |
|
matrix[i][j] = Math.min( |
|
matrix[i - 1][j - 1] + 1, |
|
matrix[i][j - 1] + 1, |
|
matrix[i - 1][j] + 1 |
|
); |
|
} |
|
} |
|
} |
|
return matrix[b.length][a.length]; |
|
} |
|
|
|
|
|
function SourcePopup({ |
|
sourceData, |
|
excerptsData, |
|
position, |
|
onMouseEnter, |
|
onMouseLeave, |
|
statementText |
|
}) { |
|
if (!sourceData || !position) return null; |
|
|
|
const domain = getDomainName(sourceData.link); |
|
let hostname = ''; |
|
try { |
|
hostname = sourceData.link ? new URL(sourceData.link).hostname : ''; |
|
} catch (err) { |
|
hostname = sourceData.link || ''; |
|
} |
|
|
|
let displayExcerpt = null; |
|
const sourceIdStr = String(sourceData.id); |
|
|
|
|
|
if (excerptsData && Array.isArray(excerptsData) && statementText) { |
|
let foundExcerpt = null; |
|
let foundByFuzzy = false; |
|
const norm = s => s.replace(/\s+/g, ' ').trim(); |
|
const lower = s => norm(s).toLowerCase(); |
|
const statementNorm = norm(statementText); |
|
const statementLower = lower(statementText); |
|
console.log(`[SourcePopup] Searching for excerpt for source ID ${sourceIdStr}: ${statementText}`); |
|
|
|
|
|
for (const entry of excerptsData) { |
|
const [thisStatement, sourcesMap] = Object.entries(entry)[0]; |
|
const thisNorm = norm(thisStatement); |
|
const thisLower = lower(thisStatement); |
|
console.log(`[SourcePopup] Checking against statement: ${thisStatement}`); |
|
|
|
|
|
if (thisNorm === statementNorm && sourcesMap && sourceIdStr in sourcesMap) { |
|
foundExcerpt = sourcesMap[sourceIdStr]; |
|
break; |
|
} |
|
|
|
if (thisLower === statementLower && sourcesMap && sourceIdStr in sourcesMap) { |
|
foundExcerpt = sourcesMap[sourceIdStr]; |
|
break; |
|
} |
|
|
|
if ( |
|
(statementNorm && thisNorm && statementNorm.includes(thisNorm)) || |
|
(thisNorm && statementNorm && thisNorm.includes(statementNorm)) |
|
) { |
|
if (sourcesMap && sourceIdStr in sourcesMap) { |
|
foundExcerpt = sourcesMap[sourceIdStr]; |
|
foundByFuzzy = true; |
|
break; |
|
} |
|
} |
|
|
|
if ( |
|
levenshtein(statementNorm, thisNorm) <= 5 && |
|
sourcesMap && sourceIdStr in sourcesMap |
|
) { |
|
foundExcerpt = sourcesMap[sourceIdStr]; |
|
foundByFuzzy = true; |
|
break; |
|
} |
|
} |
|
|
|
|
|
if (foundExcerpt && foundExcerpt.toLowerCase() !== 'excerpt not found') { |
|
if (foundByFuzzy) { |
|
|
|
console.log("[SourcePopup] Fuzzy match found an excerpt:", foundExcerpt); |
|
} else { |
|
|
|
console.log("[SourcePopup] Exact match found an excerpt:", foundExcerpt); |
|
} |
|
|
|
displayExcerpt = foundExcerpt; |
|
} else if (foundExcerpt) { |
|
|
|
displayExcerpt = "Relevant excerpt could not be automatically extracted."; |
|
console.log("[SourcePopup] Excerpt marked as not found or invalid type:", foundExcerpt); |
|
} else { |
|
|
|
displayExcerpt = "Excerpt not found for this citation."; |
|
console.log(`[SourcePopup] Excerpt not found for source ID ${sourceIdStr}: ${statementText}`); |
|
} |
|
} |
|
|
|
return ( |
|
<div |
|
className="source-popup" |
|
style={{ |
|
position: 'absolute', // Use absolute positioning |
|
top: `${position.top}px`, |
|
left: `${position.left}px`, |
|
transform: 'translate(-50%, -100%)', // Center above the reference |
|
zIndex: 1100, // Ensure it's above other content |
|
}} |
|
onMouseEnter={onMouseEnter} // Keep popup open when mouse enters it |
|
onMouseLeave={onMouseLeave} // Hide popup when mouse leaves it |
|
> |
|
<Card variant="outlined" className="source-popup-card"> |
|
<CardContent> |
|
<Typography variant="subtitle2" component="div" className="source-popup-title" gutterBottom> |
|
<Link href={sourceData.link} target="_blank" rel="noopener noreferrer" underline="hover" color="inherit"> |
|
{sourceData.title || 'Untitled Source'} |
|
</Link> |
|
</Typography> |
|
<Typography variant="body2" className="source-popup-link-info"> |
|
{hostname && ( |
|
<img |
|
src={`https://www.google.com/s2/favicons?domain=${hostname}&sz=16`} |
|
alt="" |
|
className="source-popup-icon" |
|
/> |
|
)} |
|
<span className="source-popup-domain">{domain}</span> |
|
</Typography> |
|
{displayExcerpt !== null && ( |
|
<Typography variant="caption" className="source-popup-excerpt" display="block" sx={{ mt: 1 }}> |
|
<Link |
|
href={`${sourceData.link}#:~:text=${encodeURIComponent(displayExcerpt)}`} |
|
target="_blank" |
|
rel="noopener noreferrer" |
|
underline="none" |
|
color="inherit" |
|
> |
|
{displayExcerpt} |
|
</Link> |
|
</Typography> |
|
)} |
|
</CardContent> |
|
</Card> |
|
</div> |
|
); |
|
}; |
|
|
|
export default SourcePopup; |