feat(ms-ai-architect): playground v3 report renderers (17 commands) [skip-docs]
17 rapport-renderers per kanonisk routing-tabell (Step 12) gruppert i 4 sub-batches: - Regulatory (6): renderAiActPyramid, renderRequirements, renderTransparency, renderFria, renderConformity, renderDpia - Security (3): renderSecurity, renderRos, renderReview - Economy (2): renderCost, renderLicense - Documentation (6): renderMigrate, renderAdr, renderSummary, renderPoc, renderUtredning, renderCompare Felles helpers: renderError (parser-fail fallback), renderEmptyState, renderMatrixHtml (5x5/6x5 grid), renderRadarSvg, renderThreatsTable, renderFindingsBlock. Wired stub erstattet med PARSERS+RENDERERS routing: handlePasteImport(commandId, markdown) henter cmd fra CATALOG, ruter via PARSERS[archetype] og RENDERERS[cmd.renderer], serialiserer til [data-report-slot=...]. Verktøy-commands (produces_report=false) får empty-state. Parse-feil renderer error-summary med strukturerte feilmeldinger. RENDERERS routing-objekt eksponert som window.__RENDERERS. Verified: 17 fixtures roundtrip parser+renderer, classify produserer .pyramide .pyramide__tier--high (aria-current på matchende tier), adr produserer dl med Status/Date/Deciders.
This commit is contained in:
parent
1034777d6b
commit
997acb190f
1 changed files with 603 additions and 7 deletions
|
|
@ -2611,17 +2611,613 @@
|
|||
window.__parseSections = parseSections;
|
||||
window.__extractField = extractField;
|
||||
|
||||
// ---- Paste-import stub (Step 12 erstatter med faktisk routing) ----
|
||||
// ============================================================
|
||||
// REPORT RENDERERS (Step 12)
|
||||
// ============================================================
|
||||
//
|
||||
// 17 renderers per kanonisk archetype-routing-tabell. Hver renderer
|
||||
// tar parsed data + slot DOM-element, og fyller slot.innerHTML med
|
||||
// markup som matcher design-system BEM-klasser (.pyramide, .matrix,
|
||||
// .findings, .rights-matrix, .capability-matrix, .distribution,
|
||||
// .verdict-block, .pipeline-cockpit, .diff, .aiact-timeline).
|
||||
//
|
||||
// Routing: RENDERERS[command.renderer] for oppslag i handlePasteImport
|
||||
// (under). Verktøy-commands (produces_report=false) får ingen renderer.
|
||||
|
||||
// ---- Felles helpers ----
|
||||
|
||||
function renderEmptyState() {
|
||||
return '<div class="guide-panel guide-panel--info">' +
|
||||
'<div class="guide-panel__icon" aria-hidden="true">i</div>' +
|
||||
'<div class="guide-panel__body"><p class="guide-panel__text">Ingen data å vise — tom eller ufullstendig parsing.</p></div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderError(errors, slot) {
|
||||
const items = (errors || []).map(function (e) {
|
||||
return '<li><strong>' + escapeHtml(e.section || 'feil') + ':</strong> ' + escapeHtml(e.reason || 'Ukjent') + '</li>';
|
||||
}).join('');
|
||||
slot.innerHTML =
|
||||
'<div class="error-summary" role="alert">' +
|
||||
'<h3 class="error-summary__heading">Kunne ikke parse rapporten</h3>' +
|
||||
'<div class="error-summary__body"><p>Justér markdown-format og lim inn på nytt.</p>' +
|
||||
(items ? '<ul>' + items + '</ul>' : '') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderThreatsTable(threats) {
|
||||
if (!threats || !threats.length) return '';
|
||||
const rows = threats.map(function (t) {
|
||||
return '<tr><td>' + escapeHtml(t.id || '') + '</td><td>' + escapeHtml(t.description || '') + '</td><td>' + escapeHtml(t.severity || '') + '</td><td>' + escapeHtml(t.mitigation || '') + '</td></tr>';
|
||||
}).join('');
|
||||
return '<table class="report-table"><thead><tr><th>ID</th><th>Beskrivelse</th><th>Severity</th><th>Tiltak</th></tr></thead><tbody>' + rows + '</tbody></table>';
|
||||
}
|
||||
|
||||
function renderFindingsBlock(findings, label) {
|
||||
const items = findings.map(function (f) {
|
||||
return '<li class="findings__item">' +
|
||||
'<span class="findings__item-severity-dot" data-severity="' + escapeAttr(f.severity || 'info') + '"></span>' +
|
||||
'<span class="findings__item-id">' + escapeHtml(f.id || '') + '</span>' +
|
||||
'<span class="findings__item-title">' + escapeHtml(f.recommendation || '') + '</span>' +
|
||||
'<span class="findings__item-meta">Lokasjon: ' + escapeHtml(f.location || '—') + ' · Severity: ' + escapeHtml(f.severity || '—') + '</span>' +
|
||||
'</li>';
|
||||
}).join('');
|
||||
return '<div class="findings">' +
|
||||
'<div class="findings__list">' +
|
||||
'<div class="findings__group">' +
|
||||
'<div class="findings__group-header"><span>' + escapeHtml(label) + '</span><span>' + findings.length + '</span></div>' +
|
||||
'<ul class="findings__items">' + items + '</ul>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderMatrixHtml(data, cons_max) {
|
||||
cons_max = cons_max || 5;
|
||||
const cells = data.matrix_cells || [];
|
||||
const byPC = {};
|
||||
cells.forEach(function (c) {
|
||||
const k = c.prob + '_' + c.cons;
|
||||
if (!byPC[k]) byPC[k] = [];
|
||||
byPC[k].push(c);
|
||||
});
|
||||
const probSize = 5;
|
||||
let html = '<div class="matrix"><div class="matrix__y-label">Konsekvens</div><div class="matrix__main">';
|
||||
html += '<div class="matrix__grid" style="grid-template-rows: repeat(' + cons_max + ', 1fr) 32px;">';
|
||||
for (let cons = cons_max; cons >= 1; cons--) {
|
||||
html += '<div class="matrix__y-tick">' + cons + '</div>';
|
||||
for (let prob = 1; prob <= probSize; prob++) {
|
||||
const score = prob * cons;
|
||||
const items = byPC[prob + '_' + cons] || [];
|
||||
const bubblesHtml = items.length
|
||||
? '<div class="matrix__cell-bubbles">' +
|
||||
items.slice(0, 3).map(function (it, i) {
|
||||
return '<span class="matrix__bubble" title="' + escapeAttr(it.label || '') + '">' + (i + 1) + '</span>';
|
||||
}).join('') +
|
||||
(items.length > 3 ? '<span class="matrix__bubble matrix__bubble--count">+' + (items.length - 3) + '</span>' : '') +
|
||||
'</div>'
|
||||
: '';
|
||||
html += '<div class="matrix__cell" data-score="' + score + '">' +
|
||||
'<span class="matrix__cell-score">' + score + '</span>' + bubblesHtml +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
html += '<div class="matrix__corner"></div>';
|
||||
for (let prob = 1; prob <= probSize; prob++) {
|
||||
html += '<div class="matrix__x-tick">' + prob + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
html += '<div class="matrix__x-label">Sannsynlighet</div>';
|
||||
html += '</div></div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderRadarSvg(axes) {
|
||||
if (!axes || !axes.length) return '';
|
||||
const N = axes.length;
|
||||
const cx = 150, cy = 150, R = 100;
|
||||
const points = axes.map(function (a, i) {
|
||||
const angle = (i / N) * 2 * Math.PI - Math.PI / 2;
|
||||
const r = R * (Math.max(0, Math.min(5, a.score)) / 5);
|
||||
return (cx + r * Math.cos(angle)).toFixed(1) + ',' + (cy + r * Math.sin(angle)).toFixed(1);
|
||||
}).join(' ');
|
||||
const labels = axes.map(function (a, i) {
|
||||
const angle = (i / N) * 2 * Math.PI - Math.PI / 2;
|
||||
const x = cx + (R + 25) * Math.cos(angle);
|
||||
const y = cy + (R + 25) * Math.sin(angle);
|
||||
return '<text class="radar__label" x="' + x.toFixed(1) + '" y="' + y.toFixed(1) + '" text-anchor="middle" dominant-baseline="middle">' + escapeHtml(a.name) + '</text>';
|
||||
}).join('');
|
||||
const spokes = axes.map(function (a, i) {
|
||||
const angle = (i / N) * 2 * Math.PI - Math.PI / 2;
|
||||
const x = cx + R * Math.cos(angle);
|
||||
const y = cy + R * Math.sin(angle);
|
||||
return '<line class="radar__axis" x1="' + cx + '" y1="' + cy + '" x2="' + x.toFixed(1) + '" y2="' + y.toFixed(1) + '"/>';
|
||||
}).join('');
|
||||
return '<div class="radar"><div class="radar__chart">' +
|
||||
'<svg class="radar__svg" viewBox="0 0 300 300">' +
|
||||
'<circle class="radar__grid-line" cx="' + cx + '" cy="' + cy + '" r="' + R + '" fill="none"/>' +
|
||||
'<circle class="radar__grid-line" cx="' + cx + '" cy="' + cy + '" r="' + (R * 0.6) + '" fill="none"/>' +
|
||||
spokes + labels +
|
||||
'<polygon class="radar__series" points="' + points + '" fill="rgba(99,102,241,0.25)" stroke="currentColor" stroke-width="2"/>' +
|
||||
'</svg>' +
|
||||
'</div></div>';
|
||||
}
|
||||
|
||||
// ---- Sub-batch A: Regulatory (6) ----
|
||||
|
||||
function renderAiActPyramid(data, slot) {
|
||||
const norm = (data.risk_level || '').toLowerCase();
|
||||
let activeTier = 'minimal';
|
||||
if (/forbidden|uakseptabel|prohibited|unacceptable/.test(norm)) activeTier = 'forbidden';
|
||||
else if (/høy|high|hoy/.test(norm)) activeTier = 'high';
|
||||
else if (/begrenset|limited/.test(norm)) activeTier = 'limited';
|
||||
else if (/minimal|low/.test(norm)) activeTier = 'minimal';
|
||||
|
||||
const tiers = [
|
||||
{ id: 'forbidden', label: 'Uakseptabel risiko (Art. 5)' },
|
||||
{ id: 'high', label: 'Høyrisiko (Art. 6 + Annex III)' },
|
||||
{ id: 'limited', label: 'Begrenset risiko (Art. 50)' },
|
||||
{ id: 'minimal', label: 'Minimal risiko' }
|
||||
];
|
||||
const tiersHtml = tiers.map(function (t) {
|
||||
const active = (t.id === activeTier);
|
||||
const ariaCurrent = active ? ' aria-current="true"' : '';
|
||||
const marker = active ? ' <span class="pyramide__tier-share">← klassifisert</span>' : '';
|
||||
return '<div class="pyramide__tier pyramide__tier--' + t.id + '"' + ariaCurrent + '>' +
|
||||
'<div class="pyramide__tier-label">' + escapeHtml(t.label) + '</div>' +
|
||||
marker +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
const obligationsHtml = (data.obligations || []).length
|
||||
? '<section class="report-meta"><h4>Forpliktelser</h4><ul>' +
|
||||
data.obligations.map(function (o) { return '<li>' + escapeHtml(o) + '</li>'; }).join('') +
|
||||
'</ul></section>'
|
||||
: '';
|
||||
const meta = '<section class="report-meta"><dl>' +
|
||||
'<dt>Rolle</dt><dd>' + escapeHtml(data.role || '—') + '</dd>' +
|
||||
(data.reasoning ? '<dt>Begrunnelse</dt><dd>' + escapeHtml(data.reasoning).slice(0, 800) + '</dd>' : '') +
|
||||
'</dl></section>';
|
||||
slot.innerHTML = '<div class="pyramide">' + tiersHtml + '</div>' + meta + obligationsHtml;
|
||||
}
|
||||
|
||||
function renderRequirements(data, slot) {
|
||||
const sevForStatus = function (status) {
|
||||
const s = (status || '').toLowerCase();
|
||||
if (s === 'met') return 'low';
|
||||
if (s === 'partial') return 'medium';
|
||||
if (s === 'missing') return 'critical';
|
||||
return 'info';
|
||||
};
|
||||
const items = (data.items || []).map(function (it, idx) {
|
||||
return '<li class="findings__item" data-status="' + escapeAttr(it.status || '') + '">' +
|
||||
'<span class="findings__item-severity-dot" data-severity="' + escapeAttr(sevForStatus(it.status)) + '"></span>' +
|
||||
'<span class="findings__item-id">R-' + String(idx + 1).padStart(2, '0') + '</span>' +
|
||||
'<span class="findings__item-title">' + escapeHtml(it.requirement) + '</span>' +
|
||||
'<span class="findings__item-meta">Kilde: ' + escapeHtml(it.source_article || '—') + ' · Status: ' + escapeHtml(it.status || '—') + '</span>' +
|
||||
'</li>';
|
||||
}).join('');
|
||||
slot.innerHTML =
|
||||
'<div class="findings">' +
|
||||
'<div class="findings__list">' +
|
||||
'<div class="findings__group">' +
|
||||
'<div class="findings__group-header"><span>Krav</span><span>' + (data.items || []).length + '</span></div>' +
|
||||
'<ul class="findings__items">' + items + '</ul>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderTransparency(data, slot) {
|
||||
const sectionsHtml = (data.sections || []).map(function (s) {
|
||||
return '<section><h2>' + escapeHtml(s.heading) + '</h2><p>' + escapeHtml(s.body).replace(/\n/g, '<br>') + '</p></section>';
|
||||
}).join('');
|
||||
slot.innerHTML = '<article class="report-doc">' + sectionsHtml + '</article>';
|
||||
}
|
||||
|
||||
function renderFria(data, slot) {
|
||||
const headerHtml =
|
||||
'<div class="rights-matrix__head">' +
|
||||
'<div class="rights-matrix__head-cell rights-matrix__head-cell--name">Rettighet</div>' +
|
||||
'<div class="rights-matrix__head-cell">Impact (0-5)</div>' +
|
||||
'<div class="rights-matrix__head-cell">Tiltak</div>' +
|
||||
'</div>';
|
||||
const rowsHtml = (data.rights || []).map(function (r) {
|
||||
return '<div class="rights-matrix__row">' +
|
||||
'<div class="rights-matrix__name">' + escapeHtml(r.name) + '</div>' +
|
||||
'<div class="rights-matrix__cell" data-impact="' + escapeAttr(String(r.impact)) + '">' + r.impact + '</div>' +
|
||||
'<div class="rights-matrix__name"><div class="rights-matrix__name-meta">' + escapeHtml(r.mitigation) + '</div></div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
slot.innerHTML = '<div class="rights-matrix" style="grid-template-columns: 1fr 80px 2fr;">' + headerHtml + rowsHtml + '</div>';
|
||||
}
|
||||
|
||||
function renderConformity(data, slot) {
|
||||
const stateOf = function (status) {
|
||||
const s = (status || '').toLowerCase();
|
||||
if (s === 'passed' || s === 'met' || s === 'done') return 'passed';
|
||||
if (s === 'active' || s === 'partial' || s === 'in-progress') return 'active';
|
||||
return 'upcoming';
|
||||
};
|
||||
const dlList = data.deadlines || [];
|
||||
let timelineHtml = '';
|
||||
if (dlList.length) {
|
||||
const milestones = dlList.map(function (d, i) {
|
||||
const left = ((i + 1) / (dlList.length + 1)) * 100;
|
||||
return '<div class="aiact-timeline__milestone" data-state="' + escapeAttr(stateOf(d.status)) + '" style="left: ' + left.toFixed(1) + '%">' +
|
||||
'<div class="aiact-timeline__dot"></div>' +
|
||||
'<div class="aiact-timeline__label">' +
|
||||
'<span class="aiact-timeline__label-date">' + escapeHtml(d.date) + '</span>' +
|
||||
'<span class="aiact-timeline__label-name">' + escapeHtml(d.milestone) + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
timelineHtml =
|
||||
'<div class="aiact-timeline">' +
|
||||
'<div class="aiact-timeline__track">' +
|
||||
'<div class="aiact-timeline__progress" style="width: 0%"></div>' +
|
||||
milestones +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
const sevForStatus = function (status) {
|
||||
const s = (status || '').toLowerCase();
|
||||
if (s === 'met') return 'low';
|
||||
if (s === 'partial') return 'medium';
|
||||
if (s === 'missing') return 'critical';
|
||||
return 'info';
|
||||
};
|
||||
const items = (data.checklist || []).map(function (it, idx) {
|
||||
return '<li class="findings__item" data-status="' + escapeAttr(it.status) + '">' +
|
||||
'<span class="findings__item-severity-dot" data-severity="' + escapeAttr(sevForStatus(it.status)) + '"></span>' +
|
||||
'<span class="findings__item-id">C-' + String(idx + 1).padStart(2, '0') + '</span>' +
|
||||
'<span class="findings__item-title">' + escapeHtml(it.requirement) + '</span>' +
|
||||
'<span class="findings__item-meta">Bevis: ' + escapeHtml(it.evidence || '—') + ' · ' + escapeHtml(it.status || '—') + '</span>' +
|
||||
'</li>';
|
||||
}).join('');
|
||||
const findingsHtml =
|
||||
'<div class="findings">' +
|
||||
'<div class="findings__list">' +
|
||||
'<div class="findings__group">' +
|
||||
'<div class="findings__group-header"><span>Sjekkliste</span><span>' + (data.checklist || []).length + '</span></div>' +
|
||||
'<ul class="findings__items">' + items + '</ul>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
slot.innerHTML = timelineHtml + findingsHtml;
|
||||
}
|
||||
|
||||
function renderDpia(data, slot) {
|
||||
slot.innerHTML = renderMatrixHtml(data, 5) + renderThreatsTable(data.threats);
|
||||
}
|
||||
|
||||
// ---- Sub-batch B: Security (3) ----
|
||||
|
||||
function renderSecurity(data, slot) {
|
||||
const matrixHtml = renderMatrixHtml(data, 6);
|
||||
const radarHtml = renderRadarSvg(data.dimensions || []);
|
||||
const findingsHtml = renderFindingsBlock(data.findings || [], 'Sikkerhetsfunn');
|
||||
slot.innerHTML = matrixHtml + radarHtml + findingsHtml;
|
||||
}
|
||||
|
||||
function renderRos(data, slot) {
|
||||
const matrixHtml = renderMatrixHtml(data, 5);
|
||||
const radarHtml = renderRadarSvg(data.radar_axes || []);
|
||||
slot.innerHTML = matrixHtml + radarHtml + renderThreatsTable(data.threats);
|
||||
}
|
||||
|
||||
function renderReview(data, slot) {
|
||||
slot.innerHTML = renderFindingsBlock(data.findings || [], 'Funn');
|
||||
}
|
||||
|
||||
// ---- Sub-batch C: Economy (2) ----
|
||||
|
||||
function renderCost(data, slot) {
|
||||
const p10 = data.p10 ? data.p10.monthly : 0;
|
||||
const p50 = data.p50 ? data.p50.monthly : 0;
|
||||
const p90 = data.p90 ? data.p90.monthly : 0;
|
||||
const max = Math.max(p10, p50, p90, 1);
|
||||
const distRows = [
|
||||
{ label: 'P10 (lavt)', value: p10 },
|
||||
{ label: 'P50 (median)', value: p50 },
|
||||
{ label: 'P90 (høyt)', value: p90 }
|
||||
].map(function (r) {
|
||||
const w = (r.value / max) * 100;
|
||||
return '<div class="distribution__row">' +
|
||||
'<div class="distribution__label">' + escapeHtml(r.label) + '</div>' +
|
||||
'<div class="distribution__track">' +
|
||||
'<div class="distribution__band" style="left: 0%; width: ' + w.toFixed(1) + '%"></div>' +
|
||||
'<div class="distribution__median" style="left: ' + w.toFixed(1) + '%">' +
|
||||
'<span class="distribution__median-label">' + r.value.toLocaleString('nb-NO') + ' NOK</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
const distHtml =
|
||||
'<div class="distribution">' + distRows +
|
||||
'<div class="distribution__axis"><div class="distribution__axis-ticks">' +
|
||||
'<span>0</span><span>' + Math.floor(max / 2).toLocaleString('nb-NO') + '</span><span>' + max.toLocaleString('nb-NO') + ' NOK/mnd</span>' +
|
||||
'</div></div>' +
|
||||
'</div>';
|
||||
const breakdownRows = (data.monthly_breakdown || []).map(function (m) {
|
||||
return '<tr><td>' + escapeHtml(m.component) + '</td><td>' + m.cost.toLocaleString('nb-NO') + ' NOK</td></tr>';
|
||||
}).join('');
|
||||
const breakdownHtml = breakdownRows
|
||||
? '<table class="report-table"><thead><tr><th>Komponent</th><th>NOK/mnd</th></tr></thead><tbody>' + breakdownRows + '</tbody></table>'
|
||||
: '';
|
||||
const tcoHeaders = data.tco_headers || [];
|
||||
const tcoHeader = tcoHeaders.map(function (h) { return '<th>' + escapeHtml(h) + '</th>'; }).join('');
|
||||
const tcoRows = (data.tco_table || []).map(function (r) {
|
||||
const cells = tcoHeaders.map(function (h) { return '<td>' + escapeHtml(r[h] || '') + '</td>'; }).join('');
|
||||
return '<tr>' + cells + '</tr>';
|
||||
}).join('');
|
||||
const tcoHtml = tcoRows
|
||||
? '<table class="report-table"><thead><tr>' + tcoHeader + '</tr></thead><tbody>' + tcoRows + '</tbody></table>'
|
||||
: '';
|
||||
slot.innerHTML = distHtml + breakdownHtml + tcoHtml;
|
||||
}
|
||||
|
||||
function renderLicense(data, slot) {
|
||||
const licenses = data.licenses || [];
|
||||
if (!licenses.length) { slot.innerHTML = renderEmptyState(); return; }
|
||||
const headHtml =
|
||||
'<div class="capability-matrix__head">' +
|
||||
'<div class="capability-matrix__head-cell capability-matrix__head-cell--name">Kapabilitet</div>' +
|
||||
licenses.map(function (l) {
|
||||
return '<div class="capability-matrix__head-cell">' + escapeHtml(l.name) + '</div>';
|
||||
}).join('') +
|
||||
'</div>';
|
||||
const capabilityNames = (licenses[0].capabilities || []).map(function (c) { return c.name; });
|
||||
const rowsHtml = capabilityNames.map(function (capName, capIdx) {
|
||||
const cells = licenses.map(function (l) {
|
||||
const cap = l.capabilities[capIdx];
|
||||
const status = (cap && cap.status) || 'missing';
|
||||
return '<div class="capability-matrix__cell" data-status="' + escapeAttr(status) + '">' +
|
||||
'<div class="capability-matrix__cell-icon"></div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
return '<div class="capability-matrix__row">' +
|
||||
'<div class="capability-matrix__name">' + escapeHtml(capName) + '</div>' +
|
||||
cells +
|
||||
'</div>';
|
||||
}).join('');
|
||||
slot.innerHTML = '<div class="capability-matrix" style="grid-template-columns: 220px repeat(' + licenses.length + ', 1fr);">' +
|
||||
headHtml + rowsHtml + '</div>';
|
||||
}
|
||||
|
||||
// ---- Sub-batch D: Documentation (6) ----
|
||||
|
||||
function renderMigrate(data, slot) {
|
||||
const phases = data.phases || [];
|
||||
if (!phases.length) { slot.innerHTML = renderEmptyState(); return; }
|
||||
const milestones = phases.map(function (p, i) {
|
||||
const left = ((i + 1) / (phases.length + 1)) * 100;
|
||||
return '<div class="aiact-timeline__milestone" data-state="upcoming" style="left: ' + left.toFixed(1) + '%">' +
|
||||
'<div class="aiact-timeline__dot"></div>' +
|
||||
'<div class="aiact-timeline__label">' +
|
||||
'<span class="aiact-timeline__label-date">' + (p.duration_weeks ? p.duration_weeks + ' uker' : '') + '</span>' +
|
||||
'<span class="aiact-timeline__label-name">' + escapeHtml(p.name) + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
const timelineHtml =
|
||||
'<div class="aiact-timeline">' +
|
||||
'<div class="aiact-timeline__track">' +
|
||||
'<div class="aiact-timeline__progress" style="width: 0%"></div>' +
|
||||
milestones +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
const detailsHtml = phases.map(function (p) {
|
||||
const ms = (p.milestones || []).map(function (m) { return '<li>' + escapeHtml(m) + '</li>'; }).join('');
|
||||
const sc = (p.success_criteria || []).map(function (s) { return '<li>' + escapeHtml(s) + '</li>'; }).join('');
|
||||
return '<section class="phase-detail">' +
|
||||
'<h3>' + escapeHtml(p.name) + ' <small>(' + (p.duration_weeks || '?') + ' uker)</small></h3>' +
|
||||
(ms ? '<h4>Milepæler</h4><ul>' + ms + '</ul>' : '') +
|
||||
(sc ? '<h4>Suksesskriterier</h4><ul>' + sc + '</ul>' : '') +
|
||||
'</section>';
|
||||
}).join('');
|
||||
const risksRows = (data.risks || []).map(function (r) {
|
||||
return '<tr><td>' + escapeHtml(r.risk || '') + '</td><td>' + escapeHtml(r.probability || '') + '</td><td>' + escapeHtml(r.consequence || '') + '</td><td>' + escapeHtml(r.mitigation || '') + '</td></tr>';
|
||||
}).join('');
|
||||
const risksHtml = risksRows
|
||||
? '<table class="report-table"><thead><tr><th>Risiko</th><th>Sannsynlighet</th><th>Konsekvens</th><th>Tiltak</th></tr></thead><tbody>' + risksRows + '</tbody></table>'
|
||||
: '';
|
||||
slot.innerHTML = timelineHtml + detailsHtml + risksHtml;
|
||||
}
|
||||
|
||||
function renderAdr(data, slot) {
|
||||
const meta =
|
||||
'<dl class="adr-meta">' +
|
||||
(data.status ? '<dt>Status</dt><dd>' + escapeHtml(data.status) + '</dd>' : '') +
|
||||
(data.date ? '<dt>Date</dt><dd>' + escapeHtml(data.date) + '</dd>' : '') +
|
||||
(data.deciders ? '<dt>Deciders</dt><dd>' + escapeHtml(data.deciders) + '</dd>' : '') +
|
||||
'</dl>';
|
||||
const sectionsHtml = (data.sections || []).map(function (s) {
|
||||
return '<section><h2>' + escapeHtml(s.heading) + '</h2><div class="adr-body">' + escapeHtml(s.body).replace(/\n/g, '<br>') + '</div></section>';
|
||||
}).join('');
|
||||
slot.innerHTML =
|
||||
'<article class="report-doc">' +
|
||||
'<h1>' + escapeHtml(data.title || 'ADR') + '</h1>' +
|
||||
meta +
|
||||
sectionsHtml +
|
||||
'</article>';
|
||||
}
|
||||
|
||||
function renderSummary(data, slot) {
|
||||
const verdictMap = {
|
||||
block: { variant: 'block', label: 'BLOCK' },
|
||||
warning: { variant: 'warning', label: 'WARNING' },
|
||||
allow: { variant: 'allow', label: 'ALLOW' }
|
||||
};
|
||||
const v = verdictMap[(data.verdict || '').toLowerCase()] || { variant: 'warning', label: (data.verdict || '?').toUpperCase() };
|
||||
const score = v.variant === 'block' ? 92 : v.variant === 'warning' ? 55 : 22;
|
||||
const verdictHtml =
|
||||
'<div class="verdict-block">' +
|
||||
'<div class="verdict-pill-lg" data-verdict="' + escapeAttr(v.variant) + '">' +
|
||||
'<div class="verdict-pill-lg__verdict">' + escapeHtml(v.label) + '</div>' +
|
||||
'<div class="verdict-pill-lg__sub">' + escapeHtml(data.sub || 'AI-vurdering') + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="risk-meter">' +
|
||||
'<div class="risk-meter__readout">' +
|
||||
'<span class="risk-meter__score">' + score + '</span>' +
|
||||
'<span class="risk-meter__band-label">heuristisk score (0-100)</span>' +
|
||||
'</div>' +
|
||||
'<div class="risk-meter__track">' +
|
||||
'<div class="risk-meter__pointer" style="left: ' + score + '%"></div>' +
|
||||
'</div>' +
|
||||
'<div class="risk-meter__bands">' +
|
||||
'<span>Allow</span><span>Notice</span><span>Warning</span><span>Block</span><span>Critical</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
const rationaleHtml = data.rationale
|
||||
? '<section><h3>Rationale</h3><p>' + escapeHtml(data.rationale).replace(/\n/g, '<br>') + '</p></section>'
|
||||
: '';
|
||||
let metricsHtml = '';
|
||||
if ((data.key_metrics || []).length) {
|
||||
const headers = data.metrics_headers || Object.keys(data.key_metrics[0] || {});
|
||||
const headerRow = headers.map(function (h) { return '<th>' + escapeHtml(h) + '</th>'; }).join('');
|
||||
const rows = data.key_metrics.map(function (m) {
|
||||
const cells = headers.map(function (h) { return '<td>' + escapeHtml(m[h] || '') + '</td>'; }).join('');
|
||||
return '<tr>' + cells + '</tr>';
|
||||
}).join('');
|
||||
metricsHtml = '<section><h3>Key Metrics</h3><table class="report-table"><thead><tr>' + headerRow + '</tr></thead><tbody>' + rows + '</tbody></table></section>';
|
||||
}
|
||||
const nextHtml = (data.next_steps || []).length
|
||||
? '<section><h3>Next Steps</h3><ul>' + data.next_steps.map(function (s) { return '<li>' + escapeHtml(s) + '</li>'; }).join('') + '</ul></section>'
|
||||
: '';
|
||||
slot.innerHTML = verdictHtml + rationaleHtml + metricsHtml + nextHtml;
|
||||
}
|
||||
|
||||
function renderPoc(data, slot) {
|
||||
const phases = data.phases || [];
|
||||
if (!phases.length) { slot.innerHTML = renderEmptyState(); return; }
|
||||
const stagesHtml = phases.map(function (p, i) {
|
||||
const num = String(i + 1).padStart(2, '0');
|
||||
const isCurrent = (i === 0);
|
||||
return '<div class="pc-stage" data-current="' + (isCurrent ? 'true' : 'false') + '">' +
|
||||
'<div class="pc-stage__num">' + num + '</div>' +
|
||||
'<div class="pc-stage__name">' + escapeHtml(p.name) + '</div>' +
|
||||
'<div class="pc-stage__state" data-state="' + (isCurrent ? 'running' : 'empty') + '">' + (p.duration_weeks || '?') + ' uker</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
const cockpitHtml = '<div class="pipeline-cockpit">' + stagesHtml + '</div>';
|
||||
const detailsHtml = phases.map(function (p) {
|
||||
const ms = (p.milestones || []).map(function (m) { return '<li>' + escapeHtml(m) + '</li>'; }).join('');
|
||||
const sc = (p.success_criteria || []).map(function (s) { return '<li>' + escapeHtml(s) + '</li>'; }).join('');
|
||||
return '<section class="phase-detail">' +
|
||||
'<h3>' + escapeHtml(p.name) + ' <small>(' + (p.duration_weeks || '?') + ' uker)</small></h3>' +
|
||||
(ms ? '<h4>Milepæler</h4><ul>' + ms + '</ul>' : '') +
|
||||
(sc ? '<h4>Suksesskriterier</h4><ul>' + sc + '</ul>' : '') +
|
||||
'</section>';
|
||||
}).join('');
|
||||
const risksRows = (data.risks || []).map(function (r) {
|
||||
return '<tr><td>' + escapeHtml(r.risk || '') + '</td><td>' + escapeHtml(r.probability || '') + '</td><td>' + escapeHtml(r.consequence || '') + '</td><td>' + escapeHtml(r.mitigation || '') + '</td></tr>';
|
||||
}).join('');
|
||||
const risksHtml = risksRows
|
||||
? '<table class="report-table"><thead><tr><th>Risiko</th><th>Sannsynlighet</th><th>Konsekvens</th><th>Tiltak</th></tr></thead><tbody>' + risksRows + '</tbody></table>'
|
||||
: '';
|
||||
slot.innerHTML = cockpitHtml + detailsHtml + risksHtml;
|
||||
}
|
||||
|
||||
function renderUtredning(data, slot) {
|
||||
const tocHtml = (data.sections || []).map(function (s, i) {
|
||||
return '<li><a href="#utr-sec-' + i + '">' + escapeHtml(s.heading) + '</a></li>';
|
||||
}).join('');
|
||||
const sectionsHtml = (data.sections || []).map(function (s, i) {
|
||||
return '<section id="utr-sec-' + i + '"><h2>' + escapeHtml(s.heading) + '</h2><div>' + escapeHtml(s.body).replace(/\n/g, '<br>') + '</div></section>';
|
||||
}).join('');
|
||||
slot.innerHTML =
|
||||
'<div class="report-utredning" style="display: grid; grid-template-columns: 220px 1fr; gap: var(--space-6);">' +
|
||||
'<aside class="utredning-toc"><h4>Innhold</h4><ol>' + tocHtml + '</ol></aside>' +
|
||||
'<article class="report-doc">' +
|
||||
'<h1>' + escapeHtml(data.title || 'Utredning') + '</h1>' +
|
||||
sectionsHtml +
|
||||
'</article>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderCompare(data, slot) {
|
||||
const subjects = (data.subjects && data.subjects.length === 2) ? data.subjects : ['Subjekt 1', 'Subjekt 2'];
|
||||
const firstWord = function (s) { return (s || '').toLowerCase().split(/\s+/)[0] || ''; };
|
||||
const fw1 = firstWord(subjects[0]);
|
||||
const fw2 = firstWord(subjects[1]);
|
||||
let count1 = 0, count2 = 0, lik = 0;
|
||||
(data.rows || []).forEach(function (r) {
|
||||
const w = (r.winner || '').toLowerCase();
|
||||
if (!w || /lik|begge|—|-/.test(w)) lik++;
|
||||
else if (fw1 && w.indexOf(fw1) >= 0) count1++;
|
||||
else if (fw2 && w.indexOf(fw2) >= 0) count2++;
|
||||
else lik++;
|
||||
});
|
||||
const summaryHtml =
|
||||
'<div class="diff__summary">' +
|
||||
'<div class="diff__summary-item"><span class="diff__summary-count">' + count1 + '</span> ' + escapeHtml(subjects[0]) + '</div>' +
|
||||
'<div class="diff__summary-item"><span class="diff__summary-count">' + count2 + '</span> ' + escapeHtml(subjects[1]) + '</div>' +
|
||||
'<div class="diff__summary-item"><span class="diff__summary-count">' + lik + '</span> Lik</div>' +
|
||||
'</div>';
|
||||
const headerHtml =
|
||||
'<div class="diff__row">' +
|
||||
'<div class="diff__cell diff__cell--unchanged"><strong>' + escapeHtml(subjects[0]) + '</strong></div>' +
|
||||
'<div class="diff__cell diff__cell--unchanged"><strong>' + escapeHtml(subjects[1]) + '</strong></div>' +
|
||||
'</div>';
|
||||
const rowsHtml = (data.rows || []).map(function (r) {
|
||||
const w = (r.winner || '').toLowerCase();
|
||||
let cls1 = 'diff__cell--unchanged', cls2 = 'diff__cell--unchanged';
|
||||
if (fw1 && w.indexOf(fw1) >= 0) cls1 = 'diff__cell--added';
|
||||
if (fw2 && w.indexOf(fw2) >= 0) cls2 = 'diff__cell--added';
|
||||
return '<div class="diff__row">' +
|
||||
'<div class="diff__cell ' + cls1 + '"><strong>' + escapeHtml(r.aspect) + ':</strong> ' + escapeHtml(r.value1) + '</div>' +
|
||||
'<div class="diff__cell ' + cls2 + '"><strong>' + escapeHtml(r.aspect) + ':</strong> ' + escapeHtml(r.value2) + '</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
slot.innerHTML = '<div class="diff">' + summaryHtml + headerHtml + rowsHtml + '</div>';
|
||||
}
|
||||
|
||||
// ---- RENDERERS routing-objekt (17 commands) ----
|
||||
|
||||
const RENDERERS = {
|
||||
renderAiActPyramid: renderAiActPyramid,
|
||||
renderRequirements: renderRequirements,
|
||||
renderTransparency: renderTransparency,
|
||||
renderFria: renderFria,
|
||||
renderConformity: renderConformity,
|
||||
renderDpia: renderDpia,
|
||||
renderSecurity: renderSecurity,
|
||||
renderRos: renderRos,
|
||||
renderReview: renderReview,
|
||||
renderCost: renderCost,
|
||||
renderLicense: renderLicense,
|
||||
renderMigrate: renderMigrate,
|
||||
renderAdr: renderAdr,
|
||||
renderSummary: renderSummary,
|
||||
renderPoc: renderPoc,
|
||||
renderUtredning: renderUtredning,
|
||||
renderCompare: renderCompare
|
||||
};
|
||||
window.__RENDERERS = RENDERERS;
|
||||
|
||||
// ---- Paste-import: parser + renderer routing (replaces stub) ----
|
||||
|
||||
function handlePasteImport(commandId, markdown) {
|
||||
// Stub: logger til konsoll for å verifisere DOM-kontrakten. Step 12
|
||||
// henter PARSERS[CATALOG[id].report_archetype] + RENDERERS[id], parser
|
||||
// markdown og injiserer i [data-report-slot="<id>"].
|
||||
console.log('parse-pending:', commandId, (markdown || '').slice(0, 80));
|
||||
const cmd = (CATALOG.commands || []).find(function (c) { return c.id === commandId; });
|
||||
const slot = document.querySelector('[data-report-slot="' + commandId + '"]');
|
||||
if (slot) {
|
||||
slot.innerHTML = '<div class="guide-panel guide-panel--warn"><div class="guide-panel__icon" aria-hidden="true">⏳</div><div class="guide-panel__body"><p class="guide-panel__text">Markdown mottatt (' + (markdown || '').length + ' tegn). Parser+renderer kommer i Step 12.</p></div></div>';
|
||||
if (!cmd || !cmd.produces_report) {
|
||||
if (slot) slot.innerHTML = renderEmptyState();
|
||||
return;
|
||||
}
|
||||
if (!slot) return;
|
||||
const parser = PARSERS[cmd.report_archetype];
|
||||
const renderer = RENDERERS[cmd.renderer];
|
||||
if (!parser || !renderer) {
|
||||
slot.innerHTML = '<div class="error-summary"><h3 class="error-summary__heading">Routing-feil</h3><div class="error-summary__body"><p>Mangler parser eller renderer for ' + escapeHtml(cmd.id) + '.</p></div></div>';
|
||||
return;
|
||||
}
|
||||
const result = parser(markdown);
|
||||
slot.innerHTML = '';
|
||||
if (result.ok) renderer(result.data, slot);
|
||||
else renderError(result.errors, slot);
|
||||
}
|
||||
window.__handlePasteImport = handlePasteImport;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue