/**
* DIDI Extension — Shared analysis renderer (vanilla JS, ~80% web-app parity).
*
* Drop-in for popup tab, content modal, history page. Backend-FLAT shape:
* `analysis.{verdict, techniques, ai_tampered, claims, source_assessment, ...}`.
*
* Mirrors web app PipelineAnalysis: same RISK_COLORS map (by `verdict.risk_category_color`,
* NOT by score), same HeadlineCard + ActionCallout + per-component cards layout.
*
* Exposes globally as `globalThis.DidiRender = { renderAnalysisHTML, ... }`,
* accessible as `window.DidiRender` (DOM contexts) or `self.DidiRender` (service worker).
*/
(function () {
'use strict';
// === Risk color map — identical to web app utils.ts ===
const RISK_COLORS = {
green: '#22c55e',
lightgreen: '#84cc16',
yellow: '#eab308',
orange: '#f97316',
red: '#ef4444',
darkred: '#dc2626',
};
const FALLBACK_ACCENT = '#94a3b8';
// Severity / probability colors for inner cards
const sevColor = (s) => (s >= 70 ? '#ef4444' : s >= 40 ? '#f97316' : '#eab308');
const probColor = (p) => (p >= 80 ? '#ef4444' : p >= 60 ? '#f97316' : p >= 40 ? '#eab308' : '#22c55e');
const stanceColor = (s) => (s === 'SUPPORTS' ? '#22c55e' : s === 'CONTRADICTS' ? '#ef4444' : '#94a3b8');
const claimStatusColor = (st) => ({ VT: '#22c55e', LT: '#84cc16', VF: '#ef4444', LF: '#f97316', UV: '#eab308', OP: '#94a3b8', NV: '#6b7280' }[st] || '#94a3b8');
// Category emoji icon (extension keeps emoji vs web's lucide for 80% parity)
const categoryIcon = (color) => {
if (color === 'red' || color === 'darkred') return '⛔';
if (color === 'orange') return '⚠️';
if (color === 'yellow') return 'ℹ️';
if (color === 'green' || color === 'lightgreen') return '🛡️';
return 'ℹ️';
};
const escapeHtml = (s) => String(s ?? '')
.replace(/&/g, '&').replace(//g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
const safeHostname = (url) => {
try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return ''; }
};
const truncate = (s, n) => (s && s.length > n ? s.substring(0, n) + '…' : s || '');
// Convert SCREAMING_SNAKE_CASE / snake_case to "Sentence case" for display.
// Leaves already-readable strings (with spaces or mixed case) untouched.
const humanize = (s) => {
if (!s || typeof s !== 'string') return s;
if (!/[_]/.test(s) && /[a-z]/.test(s)) return s; // already mixed-case prose
const lower = s.replace(/_/g, ' ').toLowerCase().trim();
return lower.charAt(0).toUpperCase() + lower.slice(1);
};
// === i18n — oglindă a web app (utils/i18n-enums.ts + utils/i18n-fields.ts) ===
// Extensia e în română; enum-urile backend fără câmpuri _ro se traduc prin ENUM_RO.
const ENUM_RO = {
DISINFORMATION: 'Dezinformare', UNRELIABLE: 'Nedemn de încredere', QUESTIONABLE: 'Îndoielnic',
UNCERTAIN: 'Incert', MIXED: 'Mixt', MOSTLY_TRUSTWORTHY: 'Preponderent credibil',
MOSTLY_RELIABLE: 'Preponderent credibil', TRUSTWORTHY: 'Demn de încredere', RELIABLE: 'De încredere',
INCONCLUSIVE: 'Neconcludent',
CRITICAL: 'Critic', VERY_HIGH: 'Foarte ridicat', HIGH: 'Ridicat', MODERATE: 'Moderat',
MEDIUM: 'Mediu', LOW: 'Scăzut', VERY_LOW: 'Foarte scăzut',
LIKELY_AI: 'Probabil AI', POSSIBLY_AI: 'Posibil AI', UNLIKELY_AI: 'Improbabil AI',
POSSIBLY_HUMAN: 'Posibil uman', LIKELY_HUMAN: 'Probabil uman', HUMAN: 'Uman',
TRUSTED: 'De încredere', NEUTRAL: 'Neutru', SUSPICIOUS: 'Suspect', UNTRUSTED: 'Nedemn de încredere',
SUPPORTS: 'Susține', CONTRADICTS: 'Contrazice',
official: 'Oficial', news: 'Știri', blog: 'Blog', unknown: 'Necunoscut',
NONE: 'Niciuna', CAUTION: 'Precauție', WARNING: 'Avertizare', URGENT: 'Urgent',
ESCALATE: 'Escaladează', VIRAL: 'Viral',
};
const enumLabel = (v) => (v && ENUM_RO[v]) || humanize(v || '');
// Câmp localizat cu convenția backend _ro/_en (ex. technique_name_ro).
const loc = (obj, base, fallback) => {
if (!obj) return '';
return obj[`${base}_ro`] || (fallback ? obj[`${fallback}_ro`] : '') ||
obj[`${base}_en`] || (fallback ? obj[`${fallback}_en`] : '') ||
obj[base] || (fallback ? obj[fallback] : '') || '';
};
// Nume de tehnică lizibil: preferă câmpurile localizate; dacă rămâne cheia brută
// din taxonomie (ex. "content.type_misleading"), taie prefixul de dimensiune și umanizează.
const techniqueName = (tk) => {
const named = loc(tk, 'technique_name', 'name');
if (named && !/^[a-z_]+\.[a-z_.]+$/.test(named)) return named;
const raw = named || tk.name || '';
const lastSegment = raw.split('.').pop() || raw;
return humanize(lastSegment);
};
const INPUT_TYPE_RO = { text: 'TEXT', url: 'URL', image: 'IMAGINE', audio: 'AUDIO', video: 'VIDEO' };
// === Gauge — circular SVG, identical structure to web Gauge component ===
function gaugeSVG(scorePercent, accent) {
const R = 80;
const C = 2 * Math.PI * R; // circumference
const offset = C * (1 - Math.max(0, Math.min(100, scorePercent)) / 100);
return `
${Math.round(scorePercent)}
/ 100
`;
}
// === HeadlineCard — gradient bg + gauge + category info ===
function headlineCard(verdict, accent) {
const cat = enumLabel(verdict.risk_category || 'UNKNOWN');
const lvl = enumLabel(verdict.risk_level || '');
const conf = verdict.confidence != null ? `certitudine ${Math.round(verdict.confidence)}%` : '';
const score = verdict.risk_score ?? 0;
const descr = [lvl, conf].filter(Boolean).join(' · ');
// Explicația se afișează integral (ca în web) — trunchierea tăia exact nuanțele
// de tip „afirmațiile sunt adevărate, dar prezentarea e falsificată”.
const tldrShort = (verdict.explanation_ro || verdict.explanation_en || '').trim();
return `
${gaugeSVG(score, accent)}
RISC ANALIZAT
${categoryIcon(verdict.risk_category_color)}
${escapeHtml(cat)}
${descr ? `${escapeHtml(descr)} ` : ''}
${tldrShort ? `
${escapeHtml(tldrShort)}
` : ''}
`;
}
// === ActionCallout — recommended_action with severity-tinted accent ===
function actionCallout(verdict, accent) {
const action = (verdict.recommended_action || '').trim();
if (!action) return '';
const sev = verdict.severity || '';
const eyebrow = sev === 'CRITICAL' || sev === 'HIGH' ? 'NU DISTRIBUI'
: sev === 'MEDIUM' ? 'CITEȘTE CRITIC' : 'NOTĂ';
return `
⚡
${escapeHtml(eyebrow)}
${escapeHtml(enumLabel(action))}
`;
}
// === Per-component cards ===
// Subtle technical footer shown at the bottom of each component card.
// Lists model(s) used, total LLM duration. Empty string if no signals.
function metaFooter(items) {
const parts = items.filter(Boolean);
if (!parts.length) return '';
return `
${parts.map((p) => `${escapeHtml(p)} `).join('')}
`;
}
function techniquesCard(tech) {
if (!tech) return '';
const list = tech.techniques_detected || [];
if (list.length === 0) return '';
const score = Math.round((tech.manipulation_score || 0));
const items = list.slice(0, 8).map((tk) => {
const sc = sevColor(tk.severity || 0);
const conf = tk.confidence ?? '';
const sub = loc(tk, 'subdimension_name', 'subdimension') || loc(tk, 'dimension_name', 'dimension');
const ev = tk.evidence ? truncate(tk.evidence, 140) : '';
return `
${escapeHtml(techniqueName(tk))}
${tk.severity || 0}%
${escapeHtml(humanize(sub))}${conf !== '' ? ` · încredere ${conf}%` : ''}
${ev ? `
"${escapeHtml(ev)}"
` : ''}
`;
}).join('');
const more = list.length > 8 ? `+ încă ${list.length - 8} tehnici
` : '';
const dims = (tech.dimensions_affected || []).slice(0, 6).map((d) => humanize(String(d).split('.').pop())).join(', ');
const footer = metaFooter([
tech.llm_screening && `screening: ${tech.llm_screening}`,
tech.llm_deep && `deep: ${tech.llm_deep}`,
tech.total_duration_ms && `${(tech.total_duration_ms / 1000).toFixed(1)}s`,
dims && `dimensiuni: ${dims}`,
]);
return componentCardWrap('Tehnici de manipulare', `${list.length} detectate`, score, '#f97316', items + more + footer);
}
function aiTamperedCard(ai) {
if (!ai || ai.ai_probability == null) return '';
const prob = Math.round(ai.ai_probability);
const accent = probColor(prob);
const verdict = enumLabel(ai.verdict || 'Unknown');
const disclosure = ai.disclosure_detected
? `Declarație de utilizare AI detectată${ai.disclosure_text ? ': "' + escapeHtml(truncate(ai.disclosure_text, 80)) + '"' : ''}
`
: '';
const inner = `
${prob}%
${escapeHtml(verdict)}
Probabilitate de conținut generat de AI
${disclosure}
`;
const cats = (ai.categories_affected || []).slice(0, 5).join(', ');
const footer = metaFooter([
ai.content_type && `tip: ${ai.content_type}`,
cats && `categorii: ${cats}`,
ai.indicators_count != null && `${ai.indicators_count} indicatori`,
ai.llm_screening && `screening: ${ai.llm_screening}`,
ai.llm_deep && ai.llm_deep !== 'none' && `deep: ${ai.llm_deep}`,
ai.total_duration_ms && `${(ai.total_duration_ms / 1000).toFixed(1)}s`,
]);
return componentCardWrap('Detecție AI', verdict, prob, accent, inner + footer);
}
function claimsCard(c) {
if (!c || !c.claims_verified) return '';
const list = c.claims_verified || [];
if (list.length === 0) return '';
const cred = c.credibility_score != null ? Math.round(c.credibility_score) : null;
const accent = cred == null ? '#94a3b8' : (cred >= 70 ? '#22c55e' : cred >= 40 ? '#eab308' : '#ef4444');
const summary = `
${c.verified_true ? `✓ Adevărate: ${c.verified_true} ` : ''}
${c.verified_false ? `✗ False: ${c.verified_false} ` : ''}
${c.unverified ? `? Neverificate: ${c.unverified} ` : ''}
${c.opinions ? `Opinii: ${c.opinions} ` : ''}
`;
const items = list.slice(0, 6).map((cl) => {
const sc = claimStatusColor(cl.status);
const sources = (cl.sources || []).slice(0, 3);
const srcRow = sources.length ? `
` : '';
return `
${escapeHtml(cl.text || '')}
${escapeHtml(loc(cl, 'status_name') || enumLabel(cl.status || '?'))}
${cl.reasoning ? `
${escapeHtml(truncate(cl.reasoning, 220))}
` : ''}
${srcRow}
`;
}).join('');
const more = list.length > 6 ? `+ încă ${list.length - 6} afirmații
` : '';
const meta = cred != null ? `credibilitate ${cred}%` : '';
const footer = metaFooter([
c.llm_extraction && `extragere: ${c.llm_extraction}`,
c.llm_verification && `verificare: ${c.llm_verification}`,
c.web_searches_made != null && `${c.web_searches_made} căutări web`,
c.total_duration_ms && `${(c.total_duration_ms / 1000).toFixed(1)}s`,
]);
return componentCardWrap('Verificarea afirmațiilor', meta, cred, accent, summary + items + more + footer);
}
function sourceCard(s) {
if (!s) return '';
// V3 source_assessment OR legacy domain. `s.domain` may be a string (legacy)
// or an object (extended source assessment with sub-fields) — never render
// a raw object via template interpolation.
const trust = s.trust_score ?? s.credibility_score ?? null;
const verdict = enumLabel(s.verdict || s.assessment_verdict || '');
let domain = '';
if (typeof s.domain === 'string' && s.domain) domain = s.domain;
else if (s.publication?.url) domain = safeHostname(s.publication.url);
else if (s.url) domain = safeHostname(s.url);
if (!verdict && !domain && trust == null) return '';
const accent = trust == null ? '#94a3b8' : (trust >= 70 ? '#22c55e' : trust >= 40 ? '#eab308' : '#ef4444');
const flags = (s.red_flags || []).slice(0, 5);
const warnings = (s.warnings || []).slice(0, 5);
// Pull rich publication/author/platform metadata from extended source assessment.
const pub = s.publication || {};
const author = s.author || {};
const platform = s.platform || {};
const pubName = pub.name || pub.publisher || '';
const pubTier = pub.tier || pub.credibility || '';
const authorName = author.name || author.author_name || '';
const authorClass = author.classification || author.class || author.credibility || '';
const platformName = platform.name || platform.platform_name || '';
const detailCells = [
domain && { label: 'Domeniu', value: domain, color: '#fff' },
verdict && { label: 'Verdict', value: verdict, color: accent },
s.age_category && { label: 'Vechime', value: humanize(s.age_category), color: '#fff' },
pubName && { label: 'Publicație', value: pubName + (pubTier ? ` · ${pubTier}` : ''), color: '#fff' },
authorName && { label: 'Autor', value: authorName + (authorClass ? ` · ${authorClass}` : ''), color: '#fff' },
platformName && { label: 'Platformă', value: platformName, color: '#fff' },
].filter(Boolean);
const inner = `
${detailCells.map(c => `
${escapeHtml(c.label)}
${escapeHtml(String(c.value))}
`).join('')}
${flags.length ? `Semnale de alarmă
${flags.map(f => {
const raw = typeof f === 'string' ? f : (f.description || f.flag || JSON.stringify(f));
return `
${escapeHtml(humanize(raw))}
`;
}).join('')}
` : ''}
${warnings.length ? `Avertismente
${warnings.map(w => {
const raw = typeof w === 'string' ? w : (w.description || w.warning || JSON.stringify(w));
return `
${escapeHtml(humanize(raw))}
`;
}).join('')}
` : ''}`;
const footer = metaFooter([
s.llm_extraction && `extragere: ${s.llm_extraction}`,
s.llm_evaluation && `evaluare: ${s.llm_evaluation}`,
s.total_duration_ms && `${(s.total_duration_ms / 1000).toFixed(1)}s`,
]);
return componentCardWrap('Evaluarea sursei', verdict, trust, accent, inner + footer);
}
// Generic card wrapper used by all per-component renderers.
function componentCardWrap(title, sublabel, score, accent, innerHTML) {
const scoreBlock = score != null
? `${score}% `
: '';
return `
${escapeHtml(title)}
${sublabel ? `${escapeHtml(sublabel)} ` : ''}
${scoreBlock}
${innerHTML}
`;
}
// === Input preview — small card at top showing what was analyzed ===
function inputPreview(analysis) {
const inputType = analysis.input_type || analysis.analysis_type || '';
const text = analysis.input_text;
const url = analysis.input_url;
const media = analysis.input_media_url;
if (!text && !url && !media) return '';
const label = `CONȚINUT (${INPUT_TYPE_RO[inputType] || (inputType || 'INPUT').toUpperCase()})`;
let body = '';
if (text) body = `${escapeHtml(truncate(text, 280))}
`;
else if (url) body = `${escapeHtml(url)} `;
else if (media && (inputType === 'image' || /\.(png|jpe?g|gif|webp)(\?|$)/i.test(media))) {
// Imagine analizată → miniatură, nu URL-ul brut de storage
body = `
`;
}
else if (media) body = `Deschide fișierul analizat `;
return `
${escapeHtml(label)}
${body}
`;
}
// === Main entry — return the full inner HTML for the analysis panel ===
function renderAnalysisHTML(analysis) {
if (!analysis || typeof analysis !== 'object') {
return 'Invalid analysis payload.
';
}
const verdict = analysis.verdict || {};
const accent = RISK_COLORS[verdict.risk_category_color] || FALLBACK_ACCENT;
const dur = analysis.total_duration_ms;
const inputType = analysis.input_type || analysis.analysis_type || 'text';
const sections = [
inputPreview(analysis),
headlineCard(verdict, accent),
actionCallout(verdict, accent),
techniquesCard(analysis.techniques),
aiTamperedCard(analysis.ai_tampered),
claimsCard(analysis.claims),
sourceCard(analysis.source_assessment || analysis.domain),
].filter(Boolean).join('');
const sid = analysis.session_id ? ` · sesiune ${analysis.session_id.substring(0, 8)}` : '';
const meta = `
${new Date().toLocaleString('ro-RO')} · ${escapeHtml((INPUT_TYPE_RO[inputType] || inputType).toLowerCase())}${sid}
${dur ? `${(dur / 1000).toFixed(1)}s ` : ''}
`;
return `${sections}${meta}
`;
}
// === Standalone tab page — full shell wrapping the panel ===
function renderStandalonePage(analysis) {
const inner = renderAnalysisHTML(analysis);
return `
didi · raport de analiză
didi · raport de analiză
Rezultatul analizei anti-dezinformare
${inner}
`;
}
// === In-page modal — open from content script (overlay + close handlers) ===
function renderModalHTML(analysis) {
return `
didi · raport de analiză
×
${renderAnalysisHTML(analysis)}
Am înțeles
`;
}
// Public API — exposed on globalThis so the same file works in every context:
// - popup.html / history.html: window === globalThis (DOM page)
// - content scripts (isolated world): window === globalThis
// - service worker (background.js importScripts): self === globalThis (NO window)
globalThis.DidiRender = {
RISK_COLORS,
renderAnalysisHTML,
renderStandalonePage,
renderModalHTML,
escapeHtml,
};
})();