added html helper

This commit is contained in:
Risha
2026-06-19 20:59:54 -05:00
parent ee76692961
commit c63a35dd94
2 changed files with 44 additions and 23 deletions
+33 -13
View File
@@ -2,6 +2,15 @@
console.log('[overlay] content script loaded');
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
let panel = null;
let transcriptFeedEl = null;
let interimEl = null;
@@ -73,6 +82,17 @@ function parseSpeakersFromTitle(title) {
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) {
if (!speakers.length) return 'Other';
const lower = claimText.toLowerCase();
@@ -113,10 +133,10 @@ function showSpeakerBanner(speakerId, sample) {
banner.className = 'rtfc-speaker-banner';
banner.innerHTML =
'<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">' +
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('') +
'<button class="rtfc-speaker-banner-btn rtfc-speaker-banner-btn--skip" data-id="' + speakerId + '">Skip</button>' +
'</div>';
@@ -197,7 +217,7 @@ function renderSpeakerEditor() {
el.innerHTML = speakers.map((name, i) => {
const color = getSpeakerColor(name);
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>';
}).join('');
@@ -247,7 +267,7 @@ function showError(message) {
toast.className = 'rtfc-error-toast';
toast.innerHTML =
'<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>';
toast.querySelector('.rtfc-error-close').addEventListener('click', () => toast.remove());
@@ -442,8 +462,8 @@ function buildCard(result) {
const sourcesHTML = (result.sources ?? []).map((url, i) => {
const isUrl = url.startsWith('http://') || url.startsWith('https://');
return isUrl
? '<a href="' + url + '" target="_blank" rel="noopener">Source ' + (i + 1) + '</a>'
: '<span class="rtfc-source-text">' + url + '</span>';
? '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener">Source ' + (i + 1) + '</a>'
: '<span class="rtfc-source-text">' + escapeHtml(url) + '</span>';
}).join('');
const lexicalRows = buildLexicalRows(result.lexical);
@@ -462,24 +482,24 @@ function buildCard(result) {
const speakerName = (normalizedName && !normalizedName.match(/^Speaker\s*\d+$/i)) ? normalizedName : null;
const speakerColor = speakerName ? getSpeakerColor(speakerName) : null;
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 = [
speakerTag,
'<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>' : '',
'<span class="rtfc-confidence-right">' + result.confidence + ' certainty</span>',
'<span class="rtfc-timestamp">' + (result._timestamp || '') + '</span>',
'<span class="rtfc-confidence-right">' + escapeHtml(result.confidence) + ' certainty</span>',
'<span class="rtfc-timestamp">' + escapeHtml(result._timestamp || '') + '</span>',
'</div>',
'<p class="rtfc-claim">"' + result.claim + '"</p>',
'<p class="rtfc-explanation">' + result.explanation + '</p>',
'<p class="rtfc-claim">"' + escapeHtml(result.claim) + '"</p>',
'<p class="rtfc-explanation">' + escapeHtml(result.explanation) + '</p>',
'<div class="rtfc-speaker-confidence">',
'<button class="rtfc-speaker-toggle">',
'<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>',
'</button>',
'<div class="rtfc-speaker-explanation" style="display:none">',
@@ -1,6 +1,7 @@
// session-export.js
// Handles session logging and PDF export.
// Loaded after overlay.js — exposes logVerdict(), startSession(), stopSession(), exportPDF() as globals.
// Reuses escapeHtml() defined in overlay.js (loaded first in manifest.json).
const sessionLog = [];
let sessionStartTime = null;
@@ -65,7 +66,7 @@ function exportPDF() {
const claimsHTML = speakerOrder.map((spk, spkIdx) => {
const color = spk !== 'Other' ? speakerColors[spkIdx % speakerColors.length] : '#888';
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>' +
'</div>';
@@ -78,21 +79,21 @@ function exportPDF() {
const sourcesHTML = entry.sources.length
? '<div class="sources"><span class="sources-label">Sources:</span>' +
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>'
: '';
return '<div class="claim-card">' +
'<div class="claim-header">' +
'<span class="claim-number">#' + (i + 1) + '</span>' +
'<span class="verdict" style="color:' + vcolor + '">' + entry.verdict + '</span>' +
'<span class="confidence">' + entry.confidence + ' certainty</span>' +
'<span class="timestamp">' + timestamp + '</span>' +
'<span class="verdict" style="color:' + vcolor + '">' + escapeHtml(entry.verdict) + '</span>' +
'<span class="confidence">' + escapeHtml(entry.confidence) + ' certainty</span>' +
'<span class="timestamp">' + escapeHtml(timestamp) + '</span>' +
'</div>' +
'<div class="claim-text">"' + entry.claim + '"</div>' +
'<div class="explanation">' + entry.explanation + '</div>' +
'<div class="claim-text">"' + escapeHtml(entry.claim) + '"</div>' +
'<div class="explanation">' + escapeHtml(entry.explanation) + '</div>' +
'<div class="speaker-row"><span class="speaker-label">Speaker conviction:</span> ' +
(entry.speakerConfidence || 'N/A') +
escapeHtml(entry.speakerConfidence || 'N/A') +
'</div>' +
sourcesHTML +
'</div>';
@@ -142,8 +143,8 @@ function exportPDF() {
'<div class="report-header">' +
'<div class="report-title">Fact Check Report</div>' +
'<div class="report-meta">' +
'<span>📺 ' + pageTitle + '</span>' +
'<span>🕐 ' + exportDate + '</span>' +
'<span>📺 ' + escapeHtml(pageTitle) + '</span>' +
'<span>🕐 ' + escapeHtml(exportDate) + '</span>' +
'<span>📋 ' + sessionLog.length + ' claim' + (sessionLog.length !== 1 ? 's' : '') + ' detected</span>' +
'</div>' +
'</div>' +