/** * 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 ? `
${sources.map((s) => { const stc = stanceColor(s.stance); const dom = safeHostname(s.url); return `${escapeHtml(dom)} (${escapeHtml(enumLabel(s.stance || 'NEUTRAL'))})`; }).join('')} ${(cl.sources || []).length > 3 ? `+ încă ${(cl.sources || []).length - 3}` : ''}
` : ''; 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 = ` Imaginea analizată`; } 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)}
`; } // 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, }; })();