File size: 6,713 Bytes
d5c104e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
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';

// Helper function to extract a friendly domain name from a URL.
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';
  }
};

// Helper function for Levenshtein distance calculation
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];
}

// SourcePopup component to display source information and excerpts
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 || ''; // Fallback to link if URL parsing fails
  }

  let displayExcerpt = null;
  const sourceIdStr = String(sourceData.id);

  // Find the relevant excerpt
  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}`);

    // Iterate through the list of statement-to-excerpt mappings
    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}`);

      // Normalized exact match
      if (thisNorm === statementNorm && sourcesMap && sourceIdStr in sourcesMap) {
        foundExcerpt = sourcesMap[sourceIdStr];
        break;
      }
      // Case-insensitive match
      if (thisLower === statementLower && sourcesMap && sourceIdStr in sourcesMap) {
        foundExcerpt = sourcesMap[sourceIdStr];
        break;
      }
      // Substring containment
      if (
        (statementNorm && thisNorm && statementNorm.includes(thisNorm)) ||
        (thisNorm && statementNorm && thisNorm.includes(statementNorm))
      ) {
        if (sourcesMap && sourceIdStr in sourcesMap) {
          foundExcerpt = sourcesMap[sourceIdStr];
          foundByFuzzy = true;
          break;
        }
      }
      // Levenshtein distance
      if (
        levenshtein(statementNorm, thisNorm) <= 5 &&
        sourcesMap && sourceIdStr in sourcesMap
      ) {
        foundExcerpt = sourcesMap[sourceIdStr];
        foundByFuzzy = true;
        break;
      }
    }

    // Set displayExcerpt based on what was found
    if (foundExcerpt && foundExcerpt.toLowerCase() !== 'excerpt not found') {
      if (foundByFuzzy) {
        // Fuzzy match found an excerpt
        console.log("[SourcePopup] Fuzzy match found an excerpt:", foundExcerpt);
      } else {
        // Exact match found an excerpt
        console.log("[SourcePopup] Exact match found an excerpt:", foundExcerpt);
      }
      // Exact match found an excerpt
      displayExcerpt = foundExcerpt;
    } else if (foundExcerpt) {
      // Handle case where LLM explicitly said "Excerpt not found"
      displayExcerpt = "Relevant excerpt could not be automatically extracted.";
      console.log("[SourcePopup] Excerpt marked as not found or invalid type:", foundExcerpt);
    } else {
      // Excerpt for this specific source ID wasn't found in the loaded data
      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;