companies.html diventa il cruscotto dello studio consulente: barra KPI (aziende, compliance media, scadenze in ritardo/in arrivo), griglia aziende (compliance/ rischi/incidenti + badge ritardo, clic = entra nell'azienda) e sezione "Scadenze generali" aggregata su TUTTE le aziende ordinata per data. Una sola chiamata api.consultantPortfolio(). Voce menu V3 "Le mie aziende" (icona briefcase) gata a consultant/super_admin. help.js + i18n (nav.studio + companies.*) aggiornati. Cache-buster api/topnav-v3/i18n/help ?v=20260729. version 1.25.9. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
563 lines
33 KiB
HTML
563 lines
33 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="it">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<title>AI Cross-Analysis · NIS2 Agile</title>
|
|
<!-- UI V3: Inter self-host + design system v3.css (self-host only, nessun CDN esterno). -->
|
|
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
|
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
|
<style>
|
|
:root {
|
|
--nis2-cyan: #06B6D4;
|
|
--nis2-cyan-light: #ecfeff;
|
|
--l4-purple: #7c3aed;
|
|
--l4-purple-bg: #f5f3ff;
|
|
/* compat token (ex style.css, niente CDN) */
|
|
--gray-100:#F1F5F9;--gray-200:#E3E9F0;--gray-300:#CBD5E1;--gray-400:#94A3B8;
|
|
--gray-500:#64748B;--gray-600:#475569;--gray-700:#334155;--gray-800:#1E293B;
|
|
--card-bg:#fff;--danger:#B42318;--danger-bg:#FEECEB;--warning:#B45309;--secondary:#16794A;
|
|
--border-radius:10px;--border-radius-sm:7px;--border-radius-lg:14px;
|
|
--transition:.18s ease;--font-family:inherit;
|
|
}
|
|
|
|
/* Icone inline (Lucide stroke) renderizzate via stringa: stile coerente inline */
|
|
.ico { width:1em; height:1em; vertical-align:-0.125em; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; }
|
|
.ico-lg { width:2.5rem; height:2.5rem; }
|
|
|
|
/* badge testuali pagina */
|
|
.privacy-badge { display:inline-flex; align-items:center; gap:5px; background:#d1fae5; color:#065f46; border-radius:20px; padding:3px 10px; font-size:.72rem; font-weight:700; }
|
|
.role-badge { display:inline-flex; align-items:center; gap:5px; background:var(--l4-purple-bg); color:var(--l4-purple); font-size:.75rem; font-weight:600; padding:4px 10px; border-radius:12px; }
|
|
|
|
/* ── Chat container ─────────────────────────── */
|
|
.chat-wrap { display:flex; flex-direction:column; height:calc(100vh - 320px); min-height:460px; }
|
|
.chat-messages { flex:1; overflow-y:auto; padding:16px; display:flex; flex-direction:column; gap:14px; }
|
|
.chat-input-area { border-top:1px solid var(--gray-200); padding:16px; background:var(--card-bg); border-radius:0 0 var(--border-radius-lg) var(--border-radius-lg); }
|
|
.chat-input-row { display:flex; gap:10px; align-items:flex-end; }
|
|
.chat-input-row textarea {
|
|
flex:1; min-height:56px; max-height:140px; resize:vertical; padding:10px 14px;
|
|
border:1.5px solid var(--gray-300); border-radius:var(--border-radius); font:inherit; font-size:.9rem; line-height:1.5; transition:border-color var(--transition);
|
|
}
|
|
.chat-input-row textarea:focus { outline:none; border-color:var(--l4-purple); }
|
|
.btn-send { background:var(--l4-purple); color:#fff; border:none; border-radius:var(--border-radius); padding:14px 18px; cursor:pointer; font-size:1rem; transition:background .2s; flex-shrink:0; }
|
|
.btn-send:hover:not(:disabled) { background:#6d28d9; }
|
|
.btn-send:disabled { opacity:.5; cursor:not-allowed; }
|
|
.btn-send:focus-visible { outline:3px solid #fff; outline-offset:2px; box-shadow:0 0 0 5px var(--l4-purple); }
|
|
|
|
/* ── Message bubbles ────────────────────────── */
|
|
.msg { display:flex; gap:10px; align-items:flex-start; }
|
|
.msg.user { flex-direction:row-reverse; }
|
|
.msg-avatar { width:34px; height:34px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:.85rem; flex-shrink:0; }
|
|
.msg.user .msg-avatar { background:var(--l4-purple); color:#fff; }
|
|
.msg.ai .msg-avatar { background:var(--gray-800); color:#fff; }
|
|
.msg-body { max-width:78%; flex:1; }
|
|
.msg.user .msg-body { display:flex; justify-content:flex-end; }
|
|
.msg-bubble { padding:10px 14px; border-radius:var(--border-radius); font-size:.88rem; line-height:1.55; }
|
|
.msg.user .msg-bubble { background:var(--l4-purple); color:#fff; border-radius:var(--border-radius) 0 var(--border-radius) var(--border-radius); }
|
|
.msg.ai .msg-bubble { background:var(--gray-100); color:var(--gray-800); border-radius:0 var(--border-radius) var(--border-radius) var(--border-radius); width:100%; }
|
|
|
|
/* ── AI response structured ─────────────────── */
|
|
.ai-answer { margin-bottom:12px; font-size:.88rem; line-height:1.6; }
|
|
.ai-section { margin-top:12px; }
|
|
.ai-section-title { display:flex; align-items:center; gap:6px; font-size:.72rem; font-weight:700; text-transform:uppercase; letter-spacing:.05em; color:var(--gray-500); margin-bottom:5px; }
|
|
.ai-section ul { margin:0; padding:0 0 0 16px; font-size:.84rem; color:var(--gray-700); }
|
|
.ai-section ul li { margin-bottom:3px; }
|
|
.ai-benchmark { background:var(--l4-purple-bg); border-left:3px solid var(--l4-purple); padding:8px 12px; border-radius:0 var(--border-radius-sm) var(--border-radius-sm) 0; font-size:.82rem; color:var(--gray-700); margin-top:10px; }
|
|
.ai-privacy-note { background:#fef3c7; border-left:3px solid var(--warning); padding:6px 10px; border-radius:0 var(--border-radius-sm) var(--border-radius-sm) 0; font-size:.78rem; color:#92400e; margin-top:8px; }
|
|
.msg-time { font-size:.68rem; color:var(--gray-400); margin-top:3px; text-align:right; }
|
|
.msg.ai .msg-time { text-align:left; }
|
|
|
|
/* ── Thinking bubble ────────────────────────── */
|
|
.thinking-bubble { display:flex; gap:5px; padding:12px 16px; background:var(--gray-100); border-radius:0 var(--border-radius) var(--border-radius) var(--border-radius); width:fit-content; }
|
|
.thinking-dot { width:7px; height:7px; background:var(--gray-400); border-radius:50%; animation:thinkBounce 1.2s infinite; }
|
|
.thinking-dot:nth-child(2) { animation-delay:.2s; }
|
|
.thinking-dot:nth-child(3) { animation-delay:.4s; }
|
|
@keyframes thinkBounce { 0%,60%,100% { transform:translateY(0); } 30% { transform:translateY(-8px); } }
|
|
|
|
/* ── Portfolio summary card ─────────────────── */
|
|
.portfolio-header { background:linear-gradient(135deg, #4c1d95, #6d28d9); border-radius:var(--border-radius-lg); padding:18px 22px; color:#fff; margin-bottom:16px; display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px; }
|
|
.portfolio-stat { text-align:center; }
|
|
.portfolio-stat .val { font-size:1.8rem; font-weight:800; color:#c4b5fd; }
|
|
.portfolio-stat .lbl { font-size:.68rem; opacity:.7; text-transform:uppercase; letter-spacing:.04em; }
|
|
|
|
/* ── Quick questions ────────────────────────── */
|
|
.quick-qs { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:12px; }
|
|
.quick-q { background:var(--l4-purple-bg); color:var(--l4-purple); border:1px solid #ddd6fe; border-radius:20px; padding:5px 12px; font:inherit; font-size:.77rem; cursor:pointer; transition:background .2s; }
|
|
.quick-q:hover { background:#ede9fe; }
|
|
.quick-q:focus-visible { outline:2px solid var(--l4-purple); outline-offset:2px; }
|
|
|
|
/* ── Empty state ────────────────────────────── */
|
|
.chat-empty { display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; color:var(--gray-400); text-align:center; gap:10px; }
|
|
.chat-empty .ico { color:#c4b5fd; }
|
|
.chat-empty h4 { color:var(--gray-600); font-size:.95rem; margin:0; }
|
|
.chat-empty p { font-size:.82rem; max-width:360px; margin:0; }
|
|
|
|
/* ── Sidebar tab ────────────────────────────── */
|
|
.tab-bar { display:flex; border-bottom:1px solid var(--gray-200); margin-bottom:0; flex-wrap:wrap; }
|
|
.tab-btn { display:inline-flex; align-items:center; gap:6px; padding:10px 18px; border:none; background:none; font:inherit; font-size:.84rem; font-weight:600; color:var(--gray-500); cursor:pointer; border-bottom:2px solid transparent; margin-bottom:-1px; transition:color .2s, border-color .2s; }
|
|
.tab-btn.active { color:var(--l4-purple); border-bottom-color:var(--l4-purple); }
|
|
.tab-btn:focus-visible { outline:2px solid var(--l4-purple); outline-offset:-2px; }
|
|
.tab-pane { display:none; }
|
|
.tab-pane.active { display:block; }
|
|
|
|
/* ── portfolio tab helper grids (ex style.css) ── */
|
|
.ca-grid2 { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:16px; }
|
|
.ca-sub { background:var(--card-bg); border:1px solid var(--gray-200); border-radius:var(--border-radius); overflow:hidden; }
|
|
.ca-sub-head { padding:12px 16px; border-bottom:1px solid var(--gray-100); }
|
|
.ca-sub-head h4 { margin:0; font-size:.9rem; }
|
|
.ca-sub-body { padding:14px 16px; }
|
|
.ca-cards { display:grid; grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); gap:12px; }
|
|
.ca-card { background:var(--card-bg); border:1px solid var(--gray-200); border-radius:var(--border-radius); padding:12px 14px; }
|
|
.ca-spinner { width:30px; height:30px; border:3px solid var(--gray-200); border-top-color:var(--l4-purple); border-radius:50%; animation:ca-spin .8s linear infinite; margin:30px auto; }
|
|
@keyframes ca-spin { to { transform:rotate(360deg); } }
|
|
.text-muted { color:var(--gray-500); }
|
|
</style>
|
|
<!-- PWA:start -->
|
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
<meta name="theme-color" content="#0066CC">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32.png">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16.png">
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
|
<meta name="apple-mobile-web-app-title" content="NIS2 Agile">
|
|
<meta name="application-name" content="NIS2 Agile">
|
|
<script src="/js/pwa.js?v=20260614" defer></script>
|
|
<!-- PWA:end -->
|
|
</head>
|
|
<body>
|
|
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
|
<div id="topnav"></div>
|
|
|
|
<main class="wrap">
|
|
<nav class="crumb" aria-label="breadcrumb">
|
|
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
|
<i data-lucide="chevron-right" class="ic"></i><span>AI Cross-Analysis</span>
|
|
</nav>
|
|
<div class="page-head">
|
|
<div>
|
|
<h1 style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
|
|
<i data-lucide="cpu" class="ic" style="width:24px;height:24px;color:var(--l4-purple);"></i>
|
|
AI Cross-Analysis
|
|
<span class="privacy-badge"><i data-lucide="lock" class="ic"></i> Dati aggregati — privacy garantita</span>
|
|
</h1>
|
|
<p class="sub">Analisi AI aggregata sul portfolio clienti — dati anonimizzati</p>
|
|
</div>
|
|
<span class="role-badge"><i data-lucide="user" class="ic"></i> Solo Consulenti & Admin</span>
|
|
</div>
|
|
|
|
<!-- Portfolio header -->
|
|
<div class="portfolio-header" id="portfolio-header">
|
|
<div>
|
|
<div style="font-size:0.72rem;opacity:0.6;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:2px;">Il tuo portfolio</div>
|
|
<div style="font-size:1rem;font-weight:600;">Caricamento...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="card" style="padding:0;overflow:hidden;">
|
|
<div class="tab-bar" role="tablist" aria-label="Sezioni AI Cross-Analysis">
|
|
<button class="tab-btn active" onclick="switchTab('chat')" role="tab" aria-selected="true" aria-controls="tab-chat">
|
|
<i data-lucide="message-circle" class="ic"></i> Chat AI
|
|
</button>
|
|
<button class="tab-btn" onclick="switchTab('history')" role="tab" aria-selected="false" aria-controls="tab-history">
|
|
<i data-lucide="clock" class="ic"></i> Storico
|
|
</button>
|
|
<button class="tab-btn" onclick="switchTab('portfolio')" role="tab" aria-selected="false" aria-controls="tab-portfolio">
|
|
<i data-lucide="line-chart" class="ic"></i> Portfolio dati
|
|
</button>
|
|
</div>
|
|
|
|
<!-- TAB: CHAT -->
|
|
<div class="tab-pane active" id="tab-chat" role="tabpanel">
|
|
<div class="chat-wrap">
|
|
<div class="chat-messages" id="chat-messages" aria-live="polite">
|
|
<div class="chat-empty" id="chat-empty">
|
|
<i data-lucide="cpu" class="ico ico-lg"></i>
|
|
<h4>Analisi AI del tuo portfolio clienti</h4>
|
|
<p>Fai una domanda sui dati aggregati delle tue organizzazioni. I dati vengono trasmessi in forma anonima e aggregata.</p>
|
|
</div>
|
|
</div>
|
|
<div class="chat-input-area">
|
|
<div class="quick-qs" id="quick-qs">
|
|
<button class="quick-q" onclick="setQuestion('Quali sono i gap di compliance più comuni nel mio portfolio?')">Gap più frequenti</button>
|
|
<button class="quick-q" onclick="setQuestion('Qual è il livello medio di rischio nelle mie organizzazioni?')">Livello di rischio medio</button>
|
|
<button class="quick-q" onclick="setQuestion('Quali categorie NIS2 sono meno implementate?')">Categorie NIS2 deboli</button>
|
|
<button class="quick-q" onclick="setQuestion('Quali organizzazioni hanno bisogno di intervento urgente?')">Intervento urgente</button>
|
|
<button class="quick-q" onclick="setQuestion('Com\'è lo stato della formazione nei miei clienti?')">Stato formazione</button>
|
|
<button class="quick-q" onclick="setQuestion('Benchmark: dove si posiziona il mio portfolio rispetto agli standard NIS2?')">Benchmark NIS2</button>
|
|
</div>
|
|
<div class="chat-input-row">
|
|
<label for="chat-input" class="visually-hidden" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);">Domanda all'AI sul portfolio</label>
|
|
<textarea id="chat-input" placeholder="Es: Quali sono le aree più critiche nel mio portfolio clienti?" rows="2"
|
|
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMessage();}"></textarea>
|
|
<button class="btn-send" onclick="sendMessage()" id="btn-send" aria-label="Invia domanda">
|
|
<i data-lucide="send" class="ic" style="color:#fff;"></i>
|
|
</button>
|
|
</div>
|
|
<div style="font-size:0.7rem;color:var(--gray-400);margin-top:6px;text-align:center;display:flex;align-items:center;gap:5px;justify-content:center;">
|
|
<i data-lucide="lock" class="ico"></i>
|
|
Dati anonimizzati e aggregati — nessun nome organizzazione inviato all'AI
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TAB: HISTORY -->
|
|
<div class="tab-pane" id="tab-history" role="tabpanel">
|
|
<div style="padding:16px;" id="history-content">
|
|
<div class="ca-spinner"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TAB: PORTFOLIO DATI -->
|
|
<div class="tab-pane" id="tab-portfolio" role="tabpanel">
|
|
<div style="padding:16px;" id="portfolio-content">
|
|
<div class="ca-spinner"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
|
|
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
|
|
<script src="/js/api.js?v=20260729"></script>
|
|
<script src="/js/common.js?v=20260627t"></script>
|
|
<script src="/vendor/lucide/lucide.min.js"></script>
|
|
<script src="/js/topnav-v3.js?v=20260729"></script>
|
|
<script src="/js/i18n.js?v=20260729"></script>
|
|
<script src="/js/help.js?v=20260729"></script>
|
|
<script>
|
|
// ── Icone inline (Lucide stroke) usate nelle stringhe HTML generate dal JS.
|
|
// Renderizzate immediatamente (no createIcons re-run), self-host, niente sprite BI. ──
|
|
const ICO_PATHS = {
|
|
'it-software': '<rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M9 2v2M15 2v2M9 20v2M15 20v2M20 9h2M20 14h2M2 9h2M2 14h2"/>',
|
|
'it-search': '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
|
|
'it-note': '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M16 13H8M16 17H8M10 9H8"/>',
|
|
'it-warning-circle': '<circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/>',
|
|
'it-chart-line': '<path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/>',
|
|
'it-lock': '<rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
|
|
'it-close': '<path d="M18 6 6 18M6 6l12 12"/>',
|
|
'it-user': '<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>'
|
|
};
|
|
// Compat con il vecchio helper ico(name, cls): emette SVG inline (stroke), dimensione via .ico/.ico-lg.
|
|
function ico(name, cls) {
|
|
const p = ICO_PATHS[name] || ICO_PATHS['it-software'];
|
|
return `<svg class="ico ${cls||''}" viewBox="0 0 24 24" aria-hidden="true">${p}</svg>`;
|
|
}
|
|
|
|
// ── Auth & Init ───────────────────────────────────────────────────
|
|
if (!checkAuth()) throw new Error('Not authenticated');
|
|
loadSidebar(); // = loadTopnavV3() (definito in topnav-v3.js)
|
|
if (typeof I18n !== 'undefined') I18n.init();
|
|
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
|
|
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
|
|
|
let PORTFOLIO = null; // dati aggregati portfolio
|
|
|
|
// ── Init ──────────────────────────────────────────────────────────
|
|
loadPortfolio();
|
|
|
|
async function loadPortfolio() {
|
|
try {
|
|
const r = await api.get('/cross-analysis/portfolio');
|
|
if (!r.success) {
|
|
if (r.error_code === 'INSUFFICIENT_ROLE') {
|
|
document.getElementById('portfolio-header').innerHTML = `
|
|
<div style="color:white;text-align:center;width:100%;">
|
|
${ico('it-lock','ico-lg')}<br>
|
|
<strong>Funzione riservata a Consulenti e Super Admin</strong>
|
|
</div>`;
|
|
document.querySelector('.card').innerHTML = `
|
|
<div style="padding:40px;text-align:center;color:var(--gray-500);">
|
|
${ico('it-lock','ico-lg')}
|
|
<p style="margin-top:12px;">Hai bisogno del ruolo <strong>Consultant</strong> o <strong>Super Admin</strong> per accedere all\'analisi cross-organizzazione.</p>
|
|
</div>`;
|
|
return;
|
|
}
|
|
throw new Error(r.message);
|
|
}
|
|
PORTFOLIO = r.data;
|
|
renderPortfolioHeader(PORTFOLIO);
|
|
renderPortfolioTab(PORTFOLIO);
|
|
} catch(e) {
|
|
console.error('Portfolio load error:', e);
|
|
document.getElementById('portfolio-header').innerHTML = `
|
|
<div style="color:white;opacity:0.7;">Errore caricamento portfolio</div>`;
|
|
}
|
|
}
|
|
|
|
function renderPortfolioHeader(d) {
|
|
const agg = d.aggregated || {};
|
|
const score = agg.avg_compliance_score ?? '—';
|
|
const totalRisks = agg.risks?.total_open ?? '—';
|
|
const trainPct = agg.training?.avg_completion_rate ?? '—';
|
|
document.getElementById('portfolio-header').innerHTML = `
|
|
<div>
|
|
<div style="font-size:0.72rem;opacity:0.6;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:2px;">Il tuo portfolio</div>
|
|
<div style="font-size:1.1rem;font-weight:700;">${d.org_count} organizzazioni monitorate</div>
|
|
</div>
|
|
<div class="portfolio-stat"><div class="val">${score}${score!=='—'?'%':''}</div><div class="lbl">Compliance medio</div></div>
|
|
<div class="portfolio-stat"><div class="val">${totalRisks}</div><div class="lbl">Rischi aperti</div></div>
|
|
<div class="portfolio-stat"><div class="val">${trainPct}${trainPct!=='—'?'%':''}</div><div class="lbl">Formazione media</div></div>
|
|
<div class="portfolio-stat"><div class="val">${d.org_count}</div><div class="lbl">Organizzazioni</div></div>`;
|
|
}
|
|
|
|
function renderPortfolioTab(d) {
|
|
const agg = d.aggregated;
|
|
if (!agg) {
|
|
document.getElementById('portfolio-content').innerHTML = '<p class="text-muted">Nessun dato disponibile.</p>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="ca-grid2">';
|
|
|
|
// Settori
|
|
if (agg.by_sector && Object.keys(agg.by_sector).length) {
|
|
html += `<div class="ca-sub"><div class="ca-sub-head"><h4>Distribuzione Settoriale</h4></div><div class="ca-sub-body">`;
|
|
for (const [sec, cnt] of Object.entries(agg.by_sector)) {
|
|
const pct = Math.round(cnt / d.org_count * 100);
|
|
html += `<div style="margin-bottom:8px;">
|
|
<div style="display:flex;justify-content:space-between;font-size:0.82rem;margin-bottom:3px;">
|
|
<span>${sec}</span><span style="font-weight:600;">${cnt} (${pct}%)</span>
|
|
</div>
|
|
<div style="height:4px;background:var(--gray-200);border-radius:2px;">
|
|
<div style="height:100%;width:${pct}%;background:var(--l4-purple);border-radius:2px;"></div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
html += `</div></div>`;
|
|
}
|
|
|
|
// Compliance score
|
|
if (agg.score_distribution) {
|
|
html += `<div class="ca-sub"><div class="ca-sub-head"><h4>Distribuzione Score Compliance</h4></div><div class="ca-sub-body">`;
|
|
const colors = {'0-20%':'var(--danger)','21-40%':'#f97316','41-60%':'var(--warning)','61-80%':'#84cc16','81-100%':'var(--secondary)'};
|
|
for (const [range, cnt] of Object.entries(agg.score_distribution)) {
|
|
if (cnt === 0) continue;
|
|
html += `<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px;font-size:0.82rem;">
|
|
<div style="width:8px;height:8px;border-radius:50%;background:${colors[range]||'var(--gray-400)'};flex-shrink:0;"></div>
|
|
<span style="flex:1;">${range}</span>
|
|
<strong>${cnt} org</strong>
|
|
</div>`;
|
|
}
|
|
html += `</div></div>`;
|
|
}
|
|
|
|
// Categorie NIS2 deboli
|
|
if (agg.assessments?.avg_by_category && Object.keys(agg.assessments.avg_by_category).length) {
|
|
const cats = Object.entries(agg.assessments.avg_by_category).slice(0, 5);
|
|
html += `<div class="ca-sub" style="grid-column:1/-1;"><div class="ca-sub-head"><h4>Score Medio per Categoria NIS2 (dal più basso)</h4></div><div class="ca-sub-body">`;
|
|
html += '<div class="ca-cards">';
|
|
for (const [cat, score] of cats) {
|
|
const color = score < 30 ? 'var(--danger)' : score < 60 ? 'var(--warning)' : 'var(--secondary)';
|
|
html += `<div class="ca-card">
|
|
<div style="font-size:0.75rem;color:var(--gray-500);margin-bottom:4px;">${cat}</div>
|
|
<div style="font-size:1.4rem;font-weight:800;color:${color};">${score}%</div>
|
|
<div style="height:3px;background:var(--gray-200);border-radius:2px;margin-top:6px;">
|
|
<div style="height:100%;width:${score}%;background:${color};border-radius:2px;"></div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
html += '</div></div></div>';
|
|
}
|
|
|
|
html += '</div>';
|
|
document.getElementById('portfolio-content').innerHTML = html;
|
|
}
|
|
|
|
// ── Tab switching ─────────────────────────────────────────────────
|
|
function switchTab(tab) {
|
|
const tabs = ['chat','history','portfolio'];
|
|
document.querySelectorAll('.tab-btn').forEach((b, i) => {
|
|
const active = tabs[i] === tab;
|
|
b.classList.toggle('active', active);
|
|
b.setAttribute('aria-selected', active ? 'true' : 'false');
|
|
});
|
|
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
|
|
document.getElementById('tab-' + tab).classList.add('active');
|
|
if (tab === 'history') loadHistory();
|
|
}
|
|
|
|
// ── Send message ──────────────────────────────────────────────────
|
|
let isThinking = false;
|
|
|
|
async function sendMessage() {
|
|
if (isThinking) return;
|
|
const input = document.getElementById('chat-input');
|
|
const question = input.value.trim();
|
|
if (!question) return;
|
|
|
|
// Rimuovi empty state
|
|
const empty = document.getElementById('chat-empty');
|
|
if (empty) empty.remove();
|
|
|
|
// Mostra messaggio utente
|
|
appendMessage('user', question);
|
|
input.value = '';
|
|
autoResizeTextarea(input);
|
|
|
|
// Thinking bubble
|
|
isThinking = true;
|
|
document.getElementById('btn-send').disabled = true;
|
|
const thinkId = 'think-' + Date.now();
|
|
appendThinking(thinkId);
|
|
|
|
try {
|
|
const r = await api.post('/cross-analysis/analyze', { question });
|
|
removeThinking(thinkId);
|
|
if (r.success && r.data?.result) {
|
|
appendAIResponse(r.data.result, r.data.org_count);
|
|
} else {
|
|
appendError(r.message || 'Errore sconosciuto');
|
|
}
|
|
} catch(e) {
|
|
removeThinking(thinkId);
|
|
appendError('Errore di connessione: ' + e.message);
|
|
} finally {
|
|
isThinking = false;
|
|
document.getElementById('btn-send').disabled = false;
|
|
input.focus();
|
|
}
|
|
}
|
|
|
|
function appendMessage(role, text) {
|
|
const msgs = document.getElementById('chat-messages');
|
|
const time = new Date().toLocaleTimeString('it-IT', {hour:'2-digit',minute:'2-digit'});
|
|
const iconName = role === 'user' ? 'it-user' : 'it-software';
|
|
msgs.innerHTML += `
|
|
<div class="msg ${role}">
|
|
<div class="msg-avatar">${ico(iconName)}</div>
|
|
<div class="msg-body">
|
|
<div class="msg-bubble">${escapeHtml(text)}</div>
|
|
<div class="msg-time">${time}</div>
|
|
</div>
|
|
</div>`;
|
|
msgs.scrollTop = msgs.scrollHeight;
|
|
}
|
|
|
|
function appendAIResponse(result, orgCount) {
|
|
const msgs = document.getElementById('chat-messages');
|
|
const time = new Date().toLocaleTimeString('it-IT', {hour:'2-digit',minute:'2-digit'});
|
|
|
|
let inner = `<div class="ai-answer">${escapeHtml(result.answer || 'Nessuna risposta')}</div>`;
|
|
|
|
if (result.key_findings?.length) {
|
|
inner += `<div class="ai-section">
|
|
<div class="ai-section-title">${ico('it-search')} Risultati chiave</div>
|
|
<ul>${result.key_findings.map(f => `<li>${escapeHtml(f)}</li>`).join('')}</ul>
|
|
</div>`;
|
|
}
|
|
if (result.recommendations?.length) {
|
|
inner += `<div class="ai-section">
|
|
<div class="ai-section-title">${ico('it-note')} Raccomandazioni</div>
|
|
<ul>${result.recommendations.map(r => `<li>${escapeHtml(r)}</li>`).join('')}</ul>
|
|
</div>`;
|
|
}
|
|
if (result.risk_areas?.length) {
|
|
inner += `<div class="ai-section">
|
|
<div class="ai-section-title">${ico('it-warning-circle')} Aree di attenzione</div>
|
|
<ul>${result.risk_areas.map(r => `<li>${escapeHtml(r)}</li>`).join('')}</ul>
|
|
</div>`;
|
|
}
|
|
if (result.benchmark_note) {
|
|
inner += `<div class="ai-benchmark">${ico('it-chart-line')} ${escapeHtml(result.benchmark_note)}</div>`;
|
|
}
|
|
if (result.privacy_note) {
|
|
inner += `<div class="ai-privacy-note">${ico('it-lock')} ${escapeHtml(result.privacy_note)}</div>`;
|
|
}
|
|
|
|
inner += `<div style="margin-top:8px;font-size:0.68rem;color:var(--gray-400);">Basato su ${orgCount} organizzazioni anonimizzate</div>`;
|
|
|
|
msgs.innerHTML += `
|
|
<div class="msg ai">
|
|
<div class="msg-avatar">${ico('it-software')}</div>
|
|
<div class="msg-body">
|
|
<div class="msg-bubble">${inner}</div>
|
|
<div class="msg-time">${time}</div>
|
|
</div>
|
|
</div>`;
|
|
msgs.scrollTop = msgs.scrollHeight;
|
|
}
|
|
|
|
function appendError(msg) {
|
|
const msgs = document.getElementById('chat-messages');
|
|
msgs.innerHTML += `
|
|
<div class="msg ai">
|
|
<div class="msg-avatar" style="background:var(--danger);">${ico('it-close')}</div>
|
|
<div class="msg-body">
|
|
<div class="msg-bubble" style="background:var(--danger-bg);color:var(--danger);">
|
|
${ico('it-warning-circle')} ${escapeHtml(msg)}
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
msgs.scrollTop = msgs.scrollHeight;
|
|
}
|
|
|
|
function appendThinking(id) {
|
|
const msgs = document.getElementById('chat-messages');
|
|
msgs.innerHTML += `
|
|
<div class="msg ai" id="${id}">
|
|
<div class="msg-avatar">${ico('it-software')}</div>
|
|
<div class="msg-body">
|
|
<div class="thinking-bubble">
|
|
<div class="thinking-dot"></div>
|
|
<div class="thinking-dot"></div>
|
|
<div class="thinking-dot"></div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
msgs.scrollTop = msgs.scrollHeight;
|
|
}
|
|
|
|
function removeThinking(id) {
|
|
document.getElementById(id)?.remove();
|
|
}
|
|
|
|
// ── History ───────────────────────────────────────────────────────
|
|
async function loadHistory() {
|
|
const el = document.getElementById('history-content');
|
|
el.innerHTML = '<div class="ca-spinner"></div>';
|
|
try {
|
|
const r = await api.get('/cross-analysis/history?limit=20');
|
|
if (!r.success) { el.innerHTML = '<p class="text-muted">Errore caricamento storico.</p>'; return; }
|
|
const rows = r.data?.history || [];
|
|
if (!rows.length) {
|
|
el.innerHTML = '<p class="text-muted" style="padding:20px;text-align:center;">Nessuna analisi precedente.</p>';
|
|
return;
|
|
}
|
|
el.innerHTML = rows.map(row => `
|
|
<div style="border-bottom:1px solid var(--gray-100);padding:12px 0;">
|
|
<div style="font-size:0.78rem;color:var(--gray-400);margin-bottom:4px;">
|
|
${new Date(row.created_at).toLocaleString('it-IT')}
|
|
</div>
|
|
<div style="font-size:0.84rem;font-weight:600;color:var(--gray-700);margin-bottom:4px;">
|
|
${escapeHtml((row.prompt_summary || '').replace(/^CROSS_ORG \[.*?\]: /, ''))}
|
|
</div>
|
|
<div style="font-size:0.8rem;color:var(--gray-500);">${escapeHtml(row.response_summary || '—')}</div>
|
|
</div>`).join('');
|
|
} catch(e) {
|
|
el.innerHTML = '<p class="text-muted">Errore: ' + escapeHtml(e.message) + '</p>';
|
|
}
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────
|
|
function setQuestion(q) {
|
|
document.getElementById('chat-input').value = q;
|
|
document.getElementById('chat-input').focus();
|
|
autoResizeTextarea(document.getElementById('chat-input'));
|
|
}
|
|
|
|
function autoResizeTextarea(el) {
|
|
el.style.height = 'auto';
|
|
el.style.height = Math.min(el.scrollHeight, 140) + 'px';
|
|
}
|
|
|
|
document.getElementById('chat-input').addEventListener('input', function() {
|
|
autoResizeTextarea(this);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|