added html helper
This commit is contained in:
@@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
console.log('[overlay] content script loaded');
|
console.log('[overlay] content script loaded');
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return String(s)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
let panel = null;
|
let panel = null;
|
||||||
let transcriptFeedEl = null;
|
let transcriptFeedEl = null;
|
||||||
let interimEl = null;
|
let interimEl = null;
|
||||||
@@ -73,6 +82,17 @@ function parseSpeakersFromTitle(title) {
|
|||||||
|
|
||||||
let lastActiveSpeaker = null; // track most recently labeled speaker
|
let lastActiveSpeaker = null; // track most recently labeled speaker
|
||||||
|
|
||||||
|
function normalizeSpeakerName(name) {
|
||||||
|
if (!name) return name;
|
||||||
|
// if name matches a known speaker's last name or full name, return the canonical last name
|
||||||
|
for (const speaker of speakers) {
|
||||||
|
const lastName = speaker.trim().split(' ').pop().toLowerCase();
|
||||||
|
if (name.toLowerCase() === speaker.toLowerCase()) return speaker; // exact match
|
||||||
|
if (name.toLowerCase().includes(lastName)) return speaker; // last name match
|
||||||
|
}
|
||||||
|
return name; // unknown speaker — return as-is
|
||||||
|
}
|
||||||
|
|
||||||
function getClaimSpeaker(claimText) {
|
function getClaimSpeaker(claimText) {
|
||||||
if (!speakers.length) return 'Other';
|
if (!speakers.length) return 'Other';
|
||||||
const lower = claimText.toLowerCase();
|
const lower = claimText.toLowerCase();
|
||||||
@@ -113,10 +133,10 @@ function showSpeakerBanner(speakerId, sample) {
|
|||||||
banner.className = 'rtfc-speaker-banner';
|
banner.className = 'rtfc-speaker-banner';
|
||||||
banner.innerHTML =
|
banner.innerHTML =
|
||||||
'<div class="rtfc-speaker-banner-text">New speaker detected — who is this?</div>' +
|
'<div class="rtfc-speaker-banner-text">New speaker detected — who is this?</div>' +
|
||||||
'<div class="rtfc-speaker-banner-sample">"' + sample + '..."</div>' +
|
'<div class="rtfc-speaker-banner-sample">"' + escapeHtml(sample) + '..."</div>' +
|
||||||
'<div class="rtfc-speaker-banner-buttons">' +
|
'<div class="rtfc-speaker-banner-buttons">' +
|
||||||
speakers.map(name =>
|
speakers.map(name =>
|
||||||
'<button class="rtfc-speaker-banner-btn" data-name="' + name + '" data-id="' + speakerId + '">' + name + '</button>'
|
'<button class="rtfc-speaker-banner-btn" data-name="' + escapeHtml(name) + '" data-id="' + speakerId + '">' + escapeHtml(name) + '</button>'
|
||||||
).join('') +
|
).join('') +
|
||||||
'<button class="rtfc-speaker-banner-btn rtfc-speaker-banner-btn--skip" data-id="' + speakerId + '">Skip</button>' +
|
'<button class="rtfc-speaker-banner-btn rtfc-speaker-banner-btn--skip" data-id="' + speakerId + '">Skip</button>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
@@ -197,7 +217,7 @@ function renderSpeakerEditor() {
|
|||||||
el.innerHTML = speakers.map((name, i) => {
|
el.innerHTML = speakers.map((name, i) => {
|
||||||
const color = getSpeakerColor(name);
|
const color = getSpeakerColor(name);
|
||||||
return '<span class="rtfc-speaker-chip" style="border-color:' + color + ';color:' + color + '" data-idx="' + i + '">' +
|
return '<span class="rtfc-speaker-chip" style="border-color:' + color + ';color:' + color + '" data-idx="' + i + '">' +
|
||||||
'<input class="rtfc-speaker-chip-input" value="' + name + '" data-idx="' + i + '" style="color:' + color + '" />' +
|
'<input class="rtfc-speaker-chip-input" value="' + escapeHtml(name) + '" data-idx="' + i + '" style="color:' + color + '" />' +
|
||||||
'</span>';
|
'</span>';
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
@@ -247,7 +267,7 @@ function showError(message) {
|
|||||||
toast.className = 'rtfc-error-toast';
|
toast.className = 'rtfc-error-toast';
|
||||||
toast.innerHTML =
|
toast.innerHTML =
|
||||||
'<span class="rtfc-error-icon">⚠</span>' +
|
'<span class="rtfc-error-icon">⚠</span>' +
|
||||||
'<span class="rtfc-error-msg">' + message + '</span>' +
|
'<span class="rtfc-error-msg">' + escapeHtml(message) + '</span>' +
|
||||||
'<button class="rtfc-error-close">✕</button>';
|
'<button class="rtfc-error-close">✕</button>';
|
||||||
|
|
||||||
toast.querySelector('.rtfc-error-close').addEventListener('click', () => toast.remove());
|
toast.querySelector('.rtfc-error-close').addEventListener('click', () => toast.remove());
|
||||||
@@ -442,8 +462,8 @@ function buildCard(result) {
|
|||||||
const sourcesHTML = (result.sources ?? []).map((url, i) => {
|
const sourcesHTML = (result.sources ?? []).map((url, i) => {
|
||||||
const isUrl = url.startsWith('http://') || url.startsWith('https://');
|
const isUrl = url.startsWith('http://') || url.startsWith('https://');
|
||||||
return isUrl
|
return isUrl
|
||||||
? '<a href="' + url + '" target="_blank" rel="noopener">Source ' + (i + 1) + '</a>'
|
? '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener">Source ' + (i + 1) + '</a>'
|
||||||
: '<span class="rtfc-source-text">' + url + '</span>';
|
: '<span class="rtfc-source-text">' + escapeHtml(url) + '</span>';
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
const lexicalRows = buildLexicalRows(result.lexical);
|
const lexicalRows = buildLexicalRows(result.lexical);
|
||||||
@@ -462,24 +482,24 @@ function buildCard(result) {
|
|||||||
const speakerName = (normalizedName && !normalizedName.match(/^Speaker\s*\d+$/i)) ? normalizedName : null;
|
const speakerName = (normalizedName && !normalizedName.match(/^Speaker\s*\d+$/i)) ? normalizedName : null;
|
||||||
const speakerColor = speakerName ? getSpeakerColor(speakerName) : null;
|
const speakerColor = speakerName ? getSpeakerColor(speakerName) : null;
|
||||||
if (speakerColor) {
|
if (speakerColor) {
|
||||||
speakerTag = '<div class="rtfc-speaker-tag" style="background:' + speakerColor + '">' + speakerName + '</div>';
|
speakerTag = '<div class="rtfc-speaker-tag" style="background:' + speakerColor + '">' + escapeHtml(speakerName) + '</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
card.innerHTML = [
|
card.innerHTML = [
|
||||||
speakerTag,
|
speakerTag,
|
||||||
'<div class="rtfc-verdict-header">',
|
'<div class="rtfc-verdict-header">',
|
||||||
'<span class="rtfc-badge rtfc-badge--' + color + '">' + result.verdict + '</span>',
|
'<span class="rtfc-badge rtfc-badge--' + color + '">' + escapeHtml(result.verdict) + '</span>',
|
||||||
result.pending ? '<span class="rtfc-verifying">⟳ verifying...</span>' : '',
|
result.pending ? '<span class="rtfc-verifying">⟳ verifying...</span>' : '',
|
||||||
'<span class="rtfc-confidence-right">' + result.confidence + ' certainty</span>',
|
'<span class="rtfc-confidence-right">' + escapeHtml(result.confidence) + ' certainty</span>',
|
||||||
'<span class="rtfc-timestamp">' + (result._timestamp || '') + '</span>',
|
'<span class="rtfc-timestamp">' + escapeHtml(result._timestamp || '') + '</span>',
|
||||||
'</div>',
|
'</div>',
|
||||||
'<p class="rtfc-claim">"' + result.claim + '"</p>',
|
'<p class="rtfc-claim">"' + escapeHtml(result.claim) + '"</p>',
|
||||||
'<p class="rtfc-explanation">' + result.explanation + '</p>',
|
'<p class="rtfc-explanation">' + escapeHtml(result.explanation) + '</p>',
|
||||||
'<div class="rtfc-speaker-confidence">',
|
'<div class="rtfc-speaker-confidence">',
|
||||||
'<button class="rtfc-speaker-toggle">',
|
'<button class="rtfc-speaker-toggle">',
|
||||||
'<span class="rtfc-speaker-dot rtfc-speaker-dot--' + convictionColor + '"></span>',
|
'<span class="rtfc-speaker-dot rtfc-speaker-dot--' + convictionColor + '"></span>',
|
||||||
'Speaker conviction: ' + (result.speaker_confidence || 'N/A'),
|
'Speaker conviction: ' + escapeHtml(result.speaker_confidence || 'N/A'),
|
||||||
'<span class="rtfc-speaker-arrow">▾</span>',
|
'<span class="rtfc-speaker-arrow">▾</span>',
|
||||||
'</button>',
|
'</button>',
|
||||||
'<div class="rtfc-speaker-explanation" style="display:none">',
|
'<div class="rtfc-speaker-explanation" style="display:none">',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// session-export.js
|
// session-export.js
|
||||||
// Handles session logging and PDF export.
|
// Handles session logging and PDF export.
|
||||||
// Loaded after overlay.js — exposes logVerdict(), startSession(), stopSession(), exportPDF() as globals.
|
// Loaded after overlay.js — exposes logVerdict(), startSession(), stopSession(), exportPDF() as globals.
|
||||||
|
// Reuses escapeHtml() defined in overlay.js (loaded first in manifest.json).
|
||||||
|
|
||||||
const sessionLog = [];
|
const sessionLog = [];
|
||||||
let sessionStartTime = null;
|
let sessionStartTime = null;
|
||||||
@@ -65,7 +66,7 @@ function exportPDF() {
|
|||||||
const claimsHTML = speakerOrder.map((spk, spkIdx) => {
|
const claimsHTML = speakerOrder.map((spk, spkIdx) => {
|
||||||
const color = spk !== 'Other' ? speakerColors[spkIdx % speakerColors.length] : '#888';
|
const color = spk !== 'Other' ? speakerColors[spkIdx % speakerColors.length] : '#888';
|
||||||
const headerHTML = '<div class="speaker-section-header" style="border-left:3px solid ' + color + '">' +
|
const headerHTML = '<div class="speaker-section-header" style="border-left:3px solid ' + color + '">' +
|
||||||
'<span class="speaker-section-name" style="color:' + color + '">' + spk + '</span>' +
|
'<span class="speaker-section-name" style="color:' + color + '">' + escapeHtml(spk) + '</span>' +
|
||||||
'<span class="speaker-section-count">' + speakerGroups[spk].length + ' claim' + (speakerGroups[spk].length !== 1 ? 's' : '') + '</span>' +
|
'<span class="speaker-section-count">' + speakerGroups[spk].length + ' claim' + (speakerGroups[spk].length !== 1 ? 's' : '') + '</span>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
|
|
||||||
@@ -78,21 +79,21 @@ function exportPDF() {
|
|||||||
const sourcesHTML = entry.sources.length
|
const sourcesHTML = entry.sources.length
|
||||||
? '<div class="sources"><span class="sources-label">Sources:</span>' +
|
? '<div class="sources"><span class="sources-label">Sources:</span>' +
|
||||||
entry.sources.map((url, j) =>
|
entry.sources.map((url, j) =>
|
||||||
'<a href="' + url + '" class="source-link">Source ' + (j + 1) + '</a>'
|
'<a href="' + escapeHtml(url) + '" class="source-link">Source ' + (j + 1) + '</a>'
|
||||||
).join('') + '</div>'
|
).join('') + '</div>'
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return '<div class="claim-card">' +
|
return '<div class="claim-card">' +
|
||||||
'<div class="claim-header">' +
|
'<div class="claim-header">' +
|
||||||
'<span class="claim-number">#' + (i + 1) + '</span>' +
|
'<span class="claim-number">#' + (i + 1) + '</span>' +
|
||||||
'<span class="verdict" style="color:' + vcolor + '">' + entry.verdict + '</span>' +
|
'<span class="verdict" style="color:' + vcolor + '">' + escapeHtml(entry.verdict) + '</span>' +
|
||||||
'<span class="confidence">' + entry.confidence + ' certainty</span>' +
|
'<span class="confidence">' + escapeHtml(entry.confidence) + ' certainty</span>' +
|
||||||
'<span class="timestamp">' + timestamp + '</span>' +
|
'<span class="timestamp">' + escapeHtml(timestamp) + '</span>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="claim-text">"' + entry.claim + '"</div>' +
|
'<div class="claim-text">"' + escapeHtml(entry.claim) + '"</div>' +
|
||||||
'<div class="explanation">' + entry.explanation + '</div>' +
|
'<div class="explanation">' + escapeHtml(entry.explanation) + '</div>' +
|
||||||
'<div class="speaker-row"><span class="speaker-label">Speaker conviction:</span> ' +
|
'<div class="speaker-row"><span class="speaker-label">Speaker conviction:</span> ' +
|
||||||
(entry.speakerConfidence || 'N/A') +
|
escapeHtml(entry.speakerConfidence || 'N/A') +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
sourcesHTML +
|
sourcesHTML +
|
||||||
'</div>';
|
'</div>';
|
||||||
@@ -142,8 +143,8 @@ function exportPDF() {
|
|||||||
'<div class="report-header">' +
|
'<div class="report-header">' +
|
||||||
'<div class="report-title">Fact Check Report</div>' +
|
'<div class="report-title">Fact Check Report</div>' +
|
||||||
'<div class="report-meta">' +
|
'<div class="report-meta">' +
|
||||||
'<span>📺 ' + pageTitle + '</span>' +
|
'<span>📺 ' + escapeHtml(pageTitle) + '</span>' +
|
||||||
'<span>🕐 ' + exportDate + '</span>' +
|
'<span>🕐 ' + escapeHtml(exportDate) + '</span>' +
|
||||||
'<span>📋 ' + sessionLog.length + ' claim' + (sessionLog.length !== 1 ? 's' : '') + ' detected</span>' +
|
'<span>📋 ' + sessionLog.length + ' claim' + (sessionLog.length !== 1 ? 's' : '') + ' detected</span>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
|
|||||||
Reference in New Issue
Block a user