Files
nis2-agile/public/cross-analysis.html
T

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 &amp; 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=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260731"></script>
<script src="/js/help.js?v=20260801"></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>