didi-lot3-frontend/extension/render.js
Top Clossers cec967f953 Livrare Lot 3 (Frontend): aplicație web, aplicație mobilă Android, extensie browser
- Surse complete web (React/Vite) + mobil (React Native/Expo) + extensie (MV3)
- Documentație de livrare: ghid utilizare, matrice trasabilitate cerințe, raport testare furnizor
- Artefacte binare: imagine Docker didi-frontend:lot3-1.0, APK, extensie v3.2.6 + SHA256SUMS
- Configurare adresă platformă externalizată (build args / .env / config.js)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 12:40:40 +03:00

490 lines
29 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
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 `
<div style="width:168px;height:168px;position:relative;color:${accent};flex-shrink:0;">
<svg viewBox="0 0 168 168" style="width:100%;height:100%;transform:rotate(-90deg);">
<circle cx="84" cy="84" r="${R}" fill="none" stroke="currentColor" stroke-width="6" opacity="0.12"/>
<circle cx="84" cy="84" r="${R}" fill="none" stroke="currentColor" stroke-width="6"
stroke-linecap="round" stroke-dasharray="${C.toFixed(2)}" stroke-dashoffset="${offset.toFixed(2)}"
style="transition:stroke-dashoffset 0.9s cubic-bezier(0.2,0.8,0.2,1);"/>
</svg>
<div style="position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;">
<div style="font-size:42px;font-weight:700;color:#fff;font-family:Inter,system-ui,sans-serif;line-height:1;">${Math.round(scorePercent)}</div>
<div style="font-size:11px;color:rgba(255,255,255,0.55);text-transform:uppercase;letter-spacing:1.5px;margin-top:6px;font-weight:600;">/ 100</div>
</div>
</div>`;
}
// === 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 `
<div style="display:flex;align-items:stretch;gap:28px;padding:32px 36px;
background:linear-gradient(180deg, ${accent}1a, rgba(255,255,255,0.025));
border:1px solid ${accent}33;border-radius:20px;position:relative;overflow:hidden;
font-family:Inter,system-ui,sans-serif;">
<div style="display:flex;align-items:center;justify-content:center;">
${gaugeSVG(score, accent)}
</div>
<div style="flex:1;display:flex;flex-direction:column;justify-content:center;gap:10px;min-width:0;">
<div style="font-size:11px;color:rgba(255,255,255,0.55);text-transform:uppercase;letter-spacing:1.6px;font-weight:600;">RISC ANALIZAT</div>
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<span style="display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:10px;background:${accent}26;color:${accent};font-size:18px;flex-shrink:0;">${categoryIcon(verdict.risk_category_color)}</span>
<span style="font-size:24px;font-weight:700;color:#fff;line-height:1.2;">${escapeHtml(cat)}</span>
${descr ? `<span style="font-size:13px;color:rgba(255,255,255,0.6);">${escapeHtml(descr)}</span>` : ''}
</div>
${tldrShort ? `<div style="font-family:Merriweather,Georgia,serif;font-style:italic;color:rgba(255,255,255,0.78);font-size:14px;line-height:1.55;margin-top:4px;">${escapeHtml(tldrShort)}</div>` : ''}
</div>
</div>`;
}
// === 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 `
<div style="display:flex;gap:20px;align-items:flex-start;padding:22px 26px;
background:linear-gradient(135deg, ${accent}40, ${accent}1a);
border:1px solid ${accent}66;border-radius:16px;
font-family:Inter,system-ui,sans-serif;">
<span style="flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;
width:44px;height:44px;border-radius:14px;background:${accent}33;color:${accent};font-size:22px;">⚡</span>
<div style="flex:1;min-width:0;">
<div style="font-size:11px;color:${accent};text-transform:uppercase;letter-spacing:1.4px;font-weight:700;margin-bottom:6px;">${escapeHtml(eyebrow)}</div>
<div style="font-size:15px;color:#fff;font-weight:600;line-height:1.5;">${escapeHtml(enumLabel(action))}</div>
</div>
</div>`;
}
// === 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 `<div style="margin-top:14px;padding-top:10px;border-top:1px solid rgba(255,255,255,0.05);
font-size:10px;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:0.5px;
display:flex;flex-wrap:wrap;gap:14px;">
${parts.map((p) => `<span>${escapeHtml(p)}</span>`).join('')}
</div>`;
}
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 `
<div style="background:rgba(255,255,255,0.04);border-left:3px solid ${sc};padding:10px 14px;border-radius:8px;margin-bottom:8px;">
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;">
<span style="color:#fff;font-weight:600;font-size:13px;">${escapeHtml(techniqueName(tk))}</span>
<span style="background:${sc}26;border:1px solid ${sc};color:${sc};padding:2px 9px;border-radius:5px;font-size:11px;font-weight:700;flex-shrink:0;">${tk.severity || 0}%</span>
</div>
<div style="color:rgba(255,255,255,0.5);font-size:11px;margin-top:4px;">${escapeHtml(humanize(sub))}${conf !== '' ? ` · încredere ${conf}%` : ''}</div>
${ev ? `<div style="color:rgba(255,255,255,0.6);font-size:11px;margin-top:6px;font-style:italic;border-top:1px solid rgba(255,255,255,0.06);padding-top:6px;">"${escapeHtml(ev)}"</div>` : ''}
</div>`;
}).join('');
const more = list.length > 8 ? `<div style="text-align:center;font-size:11px;color:rgba(255,255,255,0.4);margin-top:6px;">+ încă ${list.length - 8} tehnici</div>` : '';
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
? `<div style="font-size:12px;color:#22c55e;margin-top:8px;">Declarație de utilizare AI detectată${ai.disclosure_text ? ': "' + escapeHtml(truncate(ai.disclosure_text, 80)) + '"' : ''}</div>`
: '';
const inner = `
<div style="display:flex;align-items:center;gap:18px;padding:8px 4px;">
<div style="width:64px;height:64px;border-radius:50%;border:3px solid ${accent};display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:700;color:${accent};flex-shrink:0;">${prob}%</div>
<div style="flex:1;">
<div style="color:#fff;font-weight:600;font-size:14px;">${escapeHtml(verdict)}</div>
<div style="color:rgba(255,255,255,0.55);font-size:12px;margin-top:2px;">Probabilitate de conținut generat de AI</div>
${disclosure}
</div>
</div>`;
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 = `
<div style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap;">
${c.verified_true ? `<span style="background:rgba(34,197,94,0.18);border:1px solid #22c55e;color:#22c55e;padding:4px 10px;border-radius:5px;font-size:11px;font-weight:600;">✓ Adevărate: ${c.verified_true}</span>` : ''}
${c.verified_false ? `<span style="background:rgba(239,68,68,0.18);border:1px solid #ef4444;color:#ef4444;padding:4px 10px;border-radius:5px;font-size:11px;font-weight:600;">✗ False: ${c.verified_false}</span>` : ''}
${c.unverified ? `<span style="background:rgba(234,179,8,0.18);border:1px solid #eab308;color:#eab308;padding:4px 10px;border-radius:5px;font-size:11px;font-weight:600;">? Neverificate: ${c.unverified}</span>` : ''}
${c.opinions ? `<span style="background:rgba(148,163,184,0.18);border:1px solid #94a3b8;color:#94a3b8;padding:4px 10px;border-radius:5px;font-size:11px;font-weight:600;">Opinii: ${c.opinions}</span>` : ''}
</div>`;
const items = list.slice(0, 6).map((cl) => {
const sc = claimStatusColor(cl.status);
const sources = (cl.sources || []).slice(0, 3);
const srcRow = sources.length ? `
<div style="margin-top:8px;border-top:1px solid rgba(255,255,255,0.06);padding-top:8px;display:flex;flex-wrap:wrap;gap:8px;">
${sources.map((s) => {
const stc = stanceColor(s.stance);
const dom = safeHostname(s.url);
return `<a href="${escapeHtml(s.url)}" target="_blank" rel="noopener" style="font-size:11px;color:${stc};text-decoration:none;">${escapeHtml(dom)} <span style="opacity:0.6;">(${escapeHtml(enumLabel(s.stance || 'NEUTRAL'))})</span></a>`;
}).join('')}
${(cl.sources || []).length > 3 ? `<span style="font-size:11px;color:rgba(255,255,255,0.4);">+ încă ${(cl.sources || []).length - 3}</span>` : ''}
</div>` : '';
return `
<div style="background:rgba(255,255,255,0.04);border-left:3px solid ${sc};padding:12px 14px;border-radius:8px;margin-bottom:8px;">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px;">
<span style="color:#e5e7eb;font-size:13px;line-height:1.5;flex:1;">${escapeHtml(cl.text || '')}</span>
<span style="background:${sc}26;border:1px solid ${sc};color:${sc};padding:3px 9px;border-radius:5px;font-size:10px;font-weight:700;white-space:nowrap;flex-shrink:0;">${escapeHtml(loc(cl, 'status_name') || enumLabel(cl.status || '?'))}</span>
</div>
${cl.reasoning ? `<div style="color:rgba(255,255,255,0.55);font-size:12px;margin-top:8px;line-height:1.5;">${escapeHtml(truncate(cl.reasoning, 220))}</div>` : ''}
${srcRow}
</div>`;
}).join('');
const more = list.length > 6 ? `<div style="text-align:center;font-size:11px;color:rgba(255,255,255,0.4);margin-top:6px;">+ încă ${list.length - 6} afirmații</div>` : '';
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 = `
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;margin-bottom:${flags.length || warnings.length ? 14 : 0}px;">
${detailCells.map(c => `<div style="background:rgba(255,255,255,0.04);padding:10px 12px;border-radius:8px;">
<div style="font-size:10px;color:rgba(255,255,255,0.45);text-transform:uppercase;letter-spacing:1px;">${escapeHtml(c.label)}</div>
<div style="color:${c.color};font-size:13px;font-weight:600;margin-top:2px;word-break:break-word;">${escapeHtml(String(c.value))}</div>
</div>`).join('')}
</div>
${flags.length ? `<div style="margin-bottom:8px;"><div style="font-size:11px;color:#ef4444;font-weight:600;margin-bottom:6px;text-transform:uppercase;letter-spacing:0.5px;">Semnale de alarmă</div>${flags.map(f => {
const raw = typeof f === 'string' ? f : (f.description || f.flag || JSON.stringify(f));
return `<div style="font-size:12px;color:rgba(255,255,255,0.75);background:rgba(239,68,68,0.08);padding:6px 10px;border-radius:5px;margin-bottom:4px;">${escapeHtml(humanize(raw))}</div>`;
}).join('')}</div>` : ''}
${warnings.length ? `<div><div style="font-size:11px;color:#f97316;font-weight:600;margin-bottom:6px;text-transform:uppercase;letter-spacing:0.5px;">Avertismente</div>${warnings.map(w => {
const raw = typeof w === 'string' ? w : (w.description || w.warning || JSON.stringify(w));
return `<div style="font-size:12px;color:rgba(255,255,255,0.75);background:rgba(249,115,22,0.08);padding:6px 10px;border-radius:5px;margin-bottom:4px;">${escapeHtml(humanize(raw))}</div>`;
}).join('')}</div>` : ''}`;
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
? `<span style="background:${accent}26;border:1px solid ${accent};color:${accent};padding:3px 11px;border-radius:6px;font-size:12px;font-weight:700;">${score}%</span>`
: '';
return `
<div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:14px;padding:20px 22px;font-family:Inter,system-ui,sans-serif;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;gap:8px;">
<div style="font-size:14px;font-weight:600;color:#fff;">${escapeHtml(title)}</div>
<div style="display:flex;align-items:center;gap:8px;">
${sublabel ? `<span style="font-size:12px;color:rgba(255,255,255,0.5);">${escapeHtml(sublabel)}</span>` : ''}
${scoreBlock}
</div>
</div>
${innerHTML}
</div>`;
}
// === 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 = `<div style="color:rgba(255,255,255,0.85);font-size:13px;line-height:1.5;">${escapeHtml(truncate(text, 280))}</div>`;
else if (url) body = `<a href="${escapeHtml(url)}" target="_blank" rel="noopener" style="color:#1fb6bd;font-size:13px;word-break:break-all;text-decoration:none;">${escapeHtml(url)}</a>`;
else if (media && (inputType === 'image' || /\.(png|jpe?g|gif|webp)(\?|$)/i.test(media))) {
// Imagine analizată → miniatură, nu URL-ul brut de storage
body = `<a href="${escapeHtml(media)}" target="_blank" rel="noopener" style="display:inline-block;">
<img src="${escapeHtml(media)}" alt="Imaginea analizată"
style="max-width:100%;max-height:180px;border-radius:8px;border:1px solid rgba(255,255,255,0.1);display:block;"></a>`;
}
else if (media) body = `<a href="${escapeHtml(media)}" target="_blank" rel="noopener" style="color:#1fb6bd;font-size:13px;text-decoration:none;">Deschide fișierul analizat</a>`;
return `
<div style="background:rgba(255,255,255,0.025);border:1px solid rgba(255,255,255,0.05);border-radius:12px;padding:14px 18px;font-family:Inter,system-ui,sans-serif;">
<div style="font-size:10px;color:rgba(255,255,255,0.45);text-transform:uppercase;letter-spacing:1.4px;font-weight:600;margin-bottom:6px;">${escapeHtml(label)}</div>
${body}
</div>`;
}
// === Main entry — return the full inner HTML for the analysis panel ===
function renderAnalysisHTML(analysis) {
if (!analysis || typeof analysis !== 'object') {
return '<div style="padding:24px;color:#ef4444;">Invalid analysis payload.</div>';
}
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 = `
<div style="display:flex;justify-content:space-between;align-items:center;font-size:12px;color:rgba(255,255,255,0.45);margin-top:16px;font-family:Inter,system-ui,sans-serif;">
<span>${new Date().toLocaleString('ro-RO')} · ${escapeHtml((INPUT_TYPE_RO[inputType] || inputType).toLowerCase())}${sid}</span>
${dur ? `<span>${(dur / 1000).toFixed(1)}s</span>` : ''}
</div>`;
return `<div style="display:flex;flex-direction:column;gap:16px;font-family:Inter,system-ui,sans-serif;">${sections}${meta}</div>`;
}
// === Standalone tab page — full <html> shell wrapping the panel ===
function renderStandalonePage(analysis) {
const inner = renderAnalysisHTML(analysis);
return `<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<title>didi · raport de analiză</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Merriweather:ital@1&display=swap" rel="stylesheet">
<style>
*,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }
body { background:#0d1424; color:#fff; padding:32px 16px; min-height:100vh;
font-family: Inter, system-ui, -apple-system, sans-serif; }
.container { max-width: 820px; margin: 0 auto; }
h1 { font-size: 28px; font-weight: 700; margin-bottom: 6px;
background: linear-gradient(135deg, #009198 0%, #1fb6bd 100%);
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.subtitle { color: rgba(255,255,255,0.5); margin-bottom: 24px; font-size: 13px; }
a { color: inherit; }
</style>
</head>
<body>
<div class="container">
<h1>didi · raport de analiză</h1>
<div class="subtitle">Rezultatul analizei anti-dezinformare</div>
${inner}
</div>
</body>
</html>`;
}
// === In-page modal — open from content script (overlay + close handlers) ===
function renderModalHTML(analysis) {
return `
<div style="position:fixed;inset:0;background:rgba(13,20,36,0.88);z-index:99999999;
display:flex;align-items:center;justify-content:center;animation:didiFadeIn 240ms ease;">
<div style="background:#0d1424;border:1px solid rgba(0,145,152,0.25);border-radius:20px;
max-width:760px;width:92%;max-height:92vh;overflow-y:auto;
box-shadow:0 24px 80px rgba(0,0,0,0.7);padding:28px;
font-family:Inter,system-ui,sans-serif;animation:didiSlideUp 320ms cubic-bezier(0.2,0.8,0.2,1);">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
<h2 style="font-size:22px;font-weight:700;background:linear-gradient(135deg,#009198,#1fb6bd);
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;">didi · raport de analiză</h2>
<button class="didi-modal-close" style="background:rgba(255,255,255,0.06);border:none;color:#fff;
font-size:22px;width:34px;height:34px;border-radius:9px;cursor:pointer;">×</button>
</div>
${renderAnalysisHTML(analysis)}
<div style="margin-top:18px;padding-top:18px;border-top:1px solid rgba(255,255,255,0.06);">
<button class="didi-modal-gotit" style="width:100%;padding:13px;background:linear-gradient(135deg,#009198,#009198);
border:none;border-radius:10px;color:#fff;font:inherit;font-size:14px;font-weight:600;cursor:pointer;">Am înțeles</button>
</div>
</div>
</div>`;
}
// 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,
};
})();