ktg-plugin-marketplace/plugins/ms-ai-architect/playground/ms-ai-architect-playground.html
Kjell Tore Guttormsen abf2246ea1 refactor(ms-ai-architect): playground uses vendored design-system
Renames playground/azure-ai-playground.html to
playground/ms-ai-architect-playground.html (history preserved via git mv).
Old name was too narrow — plugin covers the full Microsoft AI stack
(Foundry, Copilot Studio, M365 Copilot, Power Platform, Agent Framework).

Replaces the inline <style> block with seven <link> tags pointing at the
vendored design-system under playground/vendor/playground-design-system/:
fonts.css, tokens.css, base.css, components.css, components-tier2.css,
components-tier3.css, components-tier3-supplement.css.

A small inline shim maps legacy playground tokens (--bg, --surface,
--accent, --gradient1) onto design-system tokens (--color-bg,
--color-surface, --color-primary-500, etc.), keeping all existing
playground-specific class CSS (.hero, .wizard-card, .scenario-card,
.item-card, ...) working without rewrites. <html data-theme="dark">
preserves v2's dark visual identity; light-mode toggle is deferred.

DOM, JS logic, scenario data, and command pipelines are unchanged.

Also includes .gitleaks.toml at repo root (path allowlist for vendored
MANIFEST.json files — SHA-256 file hashes are not secrets) which was
missed in the previous commit due to global git ignore.

Docs updated:
- README.md (root): notes the vendoring sync script + ms-ai-architect
  Playground subsection
- plugins/ms-ai-architect/README.md: new Playground section with sync
  workflow and standalone guarantee
- plugins/ms-ai-architect/CLAUDE.md: Playground section updated with
  vendored design-system details + new filename
2026-05-03 12:35:47 +02:00

1994 lines
128 KiB
HTML

<!DOCTYPE html>
<html lang="no" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Microsoft AI Architecture Playground</title>
<!-- Vendored design-system (shared/playground-design-system, sync via scripts/sync-design-system.mjs) -->
<link rel="stylesheet" href="./vendor/playground-design-system/fonts.css">
<link rel="stylesheet" href="./vendor/playground-design-system/tokens.css">
<link rel="stylesheet" href="./vendor/playground-design-system/base.css">
<link rel="stylesheet" href="./vendor/playground-design-system/components.css">
<link rel="stylesheet" href="./vendor/playground-design-system/components-tier2.css">
<link rel="stylesheet" href="./vendor/playground-design-system/components-tier3.css">
<link rel="stylesheet" href="./vendor/playground-design-system/components-tier3-supplement.css">
<style>
/* Legacy variable shim: maps v2 playground tokens to design-system tokens.
Keeps existing playground-specific class definitions (.hero, .wizard,
.scenario-card, etc) working without rewriting their CSS. */
:root {
--bg: var(--color-bg);
--surface: var(--color-surface);
--surface2: var(--color-surface-sunken);
--border: var(--color-border-subtle);
--text: var(--color-text-primary);
--text-dim: var(--color-text-secondary);
--accent: var(--color-primary-500);
--accent2: var(--color-state-success);
--accent3: #ff6b9d;
--accent4: #ffa726;
--accent5: #42a5f5;
--accent6: #ab47bc;
--accent7: var(--color-severity-critical);
--accent8: #66bb6a;
--gradient1: linear-gradient(135deg, var(--color-primary-500), var(--color-state-success));
--gradient2: linear-gradient(135deg, #ff6b9d, #ffa726);
}
/* Reset margin/padding on form + list elements (base.css only resets headings + p) */
ul, ol, li, button, input, select, textarea { margin: 0; padding: 0; }
ul, ol { list-style: none; }
/* Playground heading scale (intentionally overrides base.css for hero emphasis) */
h1 { font-size:2.8rem; font-weight:800; letter-spacing:-0.03em; }
h2 { font-size:1.8rem; font-weight:700; letter-spacing:-0.02em; }
h3 { font-size:1.25rem; font-weight:600; }
.container { max-width:1400px; margin:0 auto; padding:0 2rem; }
/* Hero */
.hero { text-align:center; padding:4rem 2rem 2rem; position:relative; overflow:hidden; }
.hero::before { content:''; position:absolute; top:-50%; left:-50%; width:200%; height:200%; background:radial-gradient(circle at 30% 50%, rgba(0,120,212,0.1) 0%, transparent 50%), radial-gradient(circle at 70% 50%, rgba(0,212,170,0.07) 0%, transparent 50%); }
.hero h1 { position:relative; background:var(--gradient1); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; margin-bottom:0.75rem; }
.hero p { position:relative; font-size:1.15rem; color:var(--text-dim); max-width:700px; margin:0 auto 1.5rem; }
.hero .stats { display:flex; gap:3rem; justify-content:center; position:relative; flex-wrap:wrap; margin-bottom:2rem; }
.hero .stat { text-align:center; }
.hero .stat-num { font-size:2.2rem; font-weight:800; background:var(--gradient1); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
.hero .stat-label { font-size:0.8rem; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.1em; }
.hero-modes { display:flex; gap:1rem; justify-content:center; position:relative; flex-wrap:wrap; }
.hero-mode-btn { padding:1rem 2rem; border-radius:1rem; border:2px solid var(--border); background:var(--surface); color:var(--text); cursor:pointer; font-size:1rem; font-weight:600; transition:all 0.3s; }
.hero-mode-btn:hover { border-color:var(--accent); transform:translateY(-2px); box-shadow:0 6px 24px rgba(0,120,212,0.2); }
.hero-mode-btn:first-child { border-color:var(--accent); background:rgba(0,120,212,0.1); }
/* Stepper */
.stepper { display:flex; justify-content:center; gap:0; padding:1rem 2rem; position:sticky; top:0; z-index:100; background:rgba(10,10,15,0.95); backdrop-filter:blur(20px); border-bottom:1px solid var(--border); }
.step { display:flex; align-items:center; gap:0.5rem; padding:0.6rem 1rem; cursor:pointer; opacity:0.4; transition:all 0.3s; position:relative; }
.step::after { content:''; position:absolute; right:-8px; width:16px; height:2px; background:var(--border); }
.step:last-child::after { display:none; }
.step.active { opacity:1; }
.step.completed { opacity:0.8; }
.step-circle { width:32px; height:32px; border-radius:50%; border:2px solid var(--border); display:flex; align-items:center; justify-content:center; font-size:0.85rem; font-weight:700; transition:all 0.3s; flex-shrink:0; }
.step.active .step-circle { border-color:var(--accent); background:var(--accent); color:#fff; }
.step.completed .step-circle { border-color:var(--accent2); background:var(--accent2); color:#000; }
.step-label { font-size:0.82rem; font-weight:500; color:var(--text-dim); white-space:nowrap; }
.step.active .step-label { color:var(--text); }
/* Wizard */
.wizard { max-width:800px; margin:0 auto; }
.wizard-slide { display:none; animation:fadeIn 0.3s; }
.wizard-slide.active { display:block; }
@keyframes fadeIn { from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:translateY(0)} }
.wizard h2 { text-align:center; margin-bottom:0.5rem; }
.wizard .wizard-subtitle { text-align:center; color:var(--text-dim); margin-bottom:1.5rem; font-size:0.95rem; }
.wizard-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(200px, 1fr)); gap:1rem; margin-bottom:1.5rem; }
.wizard-card { background:var(--surface); border:2px solid var(--border); border-radius:1rem; padding:1.25rem; cursor:pointer; transition:all 0.3s; text-align:center; font-weight:500; }
.wizard-card:hover { border-color:var(--accent); transform:translateY(-2px); }
.wizard-card.selected { border-color:var(--accent2); background:rgba(0,212,170,0.08); }
.wizard-card .card-icon { font-size:2rem; margin-bottom:0.5rem; }
.wizard-card .card-desc { font-size:0.78rem; color:var(--text-dim); margin-top:0.3rem; }
.wizard-checks { display:flex; flex-direction:column; gap:0.6rem; margin-bottom:1.5rem; }
.wizard-check { display:flex; align-items:center; gap:0.6rem; padding:0.6rem 1rem; background:var(--surface); border:1px solid var(--border); border-radius:0.5rem; cursor:pointer; font-size:0.9rem; }
.wizard-check:hover { border-color:var(--accent); }
.wizard-check input { accent-color:var(--accent2); }
.wizard-check.selected { border-color:var(--accent2); background:rgba(0,212,170,0.05); }
.wizard-radios { display:flex; flex-direction:column; gap:0.5rem; margin-bottom:1.5rem; }
.wizard-radio { display:flex; align-items:center; gap:0.6rem; padding:0.6rem 1rem; background:var(--surface); border:1px solid var(--border); border-radius:0.5rem; cursor:pointer; font-size:0.9rem; }
.wizard-radio:hover { border-color:var(--accent); }
.wizard-radio input { accent-color:var(--accent2); }
.wizard-inputs { display:grid; grid-template-columns:1fr 1fr; gap:1rem; margin-bottom:1.5rem; }
.wizard-field { display:flex; flex-direction:column; gap:0.3rem; }
.wizard-field label { font-size:0.82rem; color:var(--text-dim); }
.wizard-field input, .wizard-field select { padding:0.6rem; background:var(--surface); border:1px solid var(--border); color:var(--text); border-radius:0.5rem; font-size:0.9rem; }
.wizard-field input:focus, .wizard-field select:focus { border-color:var(--accent); outline:none; }
.wizard-field textarea { padding:0.6rem; background:var(--surface); border:1px solid var(--border); color:var(--text); border-radius:0.5rem; font-size:0.9rem; resize:vertical; min-height:60px; }
.wizard-nav { display:flex; justify-content:space-between; gap:1rem; margin-top:1.5rem; }
.wizard-nav button { padding:0.7rem 2rem; border-radius:0.5rem; border:none; font-weight:600; font-size:0.9rem; cursor:pointer; transition:all 0.2s; }
.wizard-prev { background:var(--surface2); color:var(--text); border:1px solid var(--border) !important; }
.wizard-prev:hover { border-color:var(--accent) !important; }
.wizard-next { background:var(--gradient1); color:#fff; }
.wizard-next:hover { opacity:0.9; }
/* Scenario Cards (reused in wizard) */
.scenarios-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(280px, 1fr)); gap:1.25rem; }
.scenario-card { background:var(--surface); border:1px solid var(--border); border-radius:1rem; padding:1.5rem; cursor:pointer; transition:all 0.3s; position:relative; }
.scenario-card:hover { border-color:var(--accent); transform:translateY(-2px); box-shadow:0 6px 24px rgba(0,120,212,0.15); }
.scenario-card.active { border-color:var(--accent2); background:rgba(0,212,170,0.05); }
.scenario-card.active::after { content:'\2713'; position:absolute; top:1rem; right:1rem; width:28px; height:28px; background:var(--accent2); color:#000; border-radius:50%; display:flex; align-items:center; justify-content:center; font-weight:700; }
.scenario-icon { font-size:2rem; margin-bottom:0.75rem; }
.scenario-card h3 { margin-bottom:0.5rem; }
.scenario-card p { font-size:0.85rem; color:var(--text-dim); margin-bottom:0.75rem; }
.scenario-items-count { font-size:0.75rem; color:var(--accent); font-weight:600; }
/* Main Layout */
.main-layout { display:grid; grid-template-columns:280px 1fr; gap:2rem; }
@media(max-width:900px) { .main-layout { grid-template-columns:1fr; } }
/* Filter Sidebar */
.sidebar { position:sticky; top:70px; align-self:start; max-height:calc(100vh - 90px); overflow-y:auto; }
.filter-group { background:var(--surface); border:1px solid var(--border); border-radius:0.75rem; padding:1.25rem; margin-bottom:1rem; }
.filter-group h4 { font-size:0.85rem; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.75rem; }
.filter-option { display:flex; align-items:center; gap:0.5rem; padding:0.35rem 0; cursor:pointer; font-size:0.85rem; }
.filter-option input[type="checkbox"] { accent-color:var(--accent); }
.filter-reset { background:none; border:1px solid var(--border); color:var(--text-dim); padding:0.4rem 0.8rem; border-radius:0.5rem; cursor:pointer; font-size:0.75rem; margin-top:0.5rem; width:100%; }
.filter-reset:hover { border-color:var(--accent7); color:var(--accent7); }
/* Aisles & Items */
.aisle { margin-bottom:2.5rem; }
.aisle-header { display:flex; align-items:center; gap:0.75rem; margin-bottom:1rem; padding-bottom:0.5rem; border-bottom:2px solid var(--border); }
.aisle-icon { font-size:1.4rem; }
.aisle-header h3 { flex:1; }
.aisle-count { font-size:0.75rem; color:var(--text-dim); background:var(--surface2); padding:0.15rem 0.6rem; border-radius:1rem; }
.items-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(280px, 1fr)); gap:0.75rem; }
.item-card { background:var(--surface2); border:1px solid var(--border); border-radius:0.75rem; padding:1rem; cursor:pointer; transition:all 0.2s; position:relative; }
.item-card:hover { border-color:var(--accent2); }
.item-card.selected { border-color:var(--accent2); background:rgba(0,212,170,0.06); }
.item-card.selected::after { content:'\2713'; position:absolute; top:0.6rem; right:0.6rem; width:22px; height:22px; background:var(--accent2); color:#000; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:0.75rem; font-weight:700; }
.item-card.filtered-out { opacity:0.2; pointer-events:none; }
.item-card.intake-mismatch { opacity:0.35; border-style:dashed; }
.item-name { font-weight:600; font-size:0.9rem; margin-bottom:0.3rem; }
.item-desc { font-size:0.78rem; color:var(--text-dim); margin-bottom:0.5rem; line-height:1.4; }
.item-meta { display:flex; gap:0.3rem; flex-wrap:wrap; align-items:center; }
.item-source { font-size:0.62rem; padding:0.1rem 0.4rem; border-radius:0.3rem; font-weight:600; }
.item-badge { font-size:0.6rem; padding:0.1rem 0.35rem; border-radius:0.2rem; border:1px solid; }
.skill-badge { font-size:0.6rem; padding:0.1rem 0.35rem; border-radius:0.2rem; font-weight:600; }
.skill-citizen { background:rgba(102,187,106,0.2); color:#a5d6a7; }
.skill-pro { background:rgba(255,167,38,0.2); color:#ffd54f; }
.skill-devops { background:rgba(239,83,80,0.2); color:#ff8a80; }
.item-why { font-size:0.72rem; color:var(--accent4); margin-top:0.4rem; font-style:italic; display:none; }
.item-card.has-reason .item-why { display:block; }
/* Brand colors */
.source-m365 { background:rgba(0,120,212,0.2); color:#4da3e8; }
.source-studio { background:rgba(0,212,170,0.2); color:#69f0ae; }
.source-foundry { background:rgba(171,71,188,0.2); color:#e1bee7; }
.source-openai { background:rgba(255,167,38,0.2); color:#ffd54f; }
.source-search { background:rgba(66,165,245,0.2); color:#90caf9; }
.source-services { background:rgba(239,83,80,0.2); color:#ff8a80; }
.source-sk { background:rgba(120,144,156,0.2); color:#b0bec5; }
.source-power { background:rgba(102,187,106,0.2); color:#a5d6a7; }
.source-graph { background:rgba(255,107,157,0.2); color:#f48fb1; }
.badge-ga { border-color:#66bb6a; color:#66bb6a; }
.badge-preview { border-color:#ffa726; color:#ffa726; }
.badge-low { border-color:#66bb6a; color:#66bb6a; }
.badge-medium { border-color:#ffa726; color:#ffa726; }
.badge-high { border-color:#ef5350; color:#ef5350; }
/* Search */
.search-bar { margin-bottom:1.5rem; }
.search-input { width:100%; padding:0.7rem 1rem; background:var(--surface); border:1px solid var(--border); border-radius:0.5rem; color:var(--text); font-size:0.9rem; outline:none; }
.search-input:focus { border-color:var(--accent); }
/* Cart FAB + Panel */
.cart-fab { position:fixed; bottom:2rem; right:2rem; z-index:200; }
.cart-btn { width:60px; height:60px; border-radius:50%; background:var(--gradient1); border:none; color:#fff; font-size:1.4rem; cursor:pointer; box-shadow:0 4px 20px rgba(0,120,212,0.4); transition:all 0.3s; display:flex; align-items:center; justify-content:center; }
.cart-btn:hover { transform:scale(1.08); }
.cart-badge { position:absolute; top:-4px; right:-4px; width:22px; height:22px; background:var(--accent3); color:#fff; border-radius:50%; font-size:0.7rem; font-weight:700; display:flex; align-items:center; justify-content:center; }
.cart-panel { position:fixed; bottom:0; right:0; width:460px; max-height:85vh; background:var(--surface); border:1px solid var(--border); border-radius:1rem 0 0 0; overflow-y:auto; transform:translateX(100%); transition:transform 0.3s; z-index:201; }
.cart-panel.open { transform:translateX(0); }
@media(max-width:768px) { .cart-panel { width:100%; border-radius:1rem 1rem 0 0; } }
.cart-header { padding:1.25rem; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; position:sticky; top:0; background:var(--surface); z-index:1; }
.cart-header h3 { background:var(--gradient1); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
.cart-close { background:none; border:none; color:var(--text-dim); font-size:1.5rem; cursor:pointer; }
.cart-body { padding:1rem; }
.cart-item { display:flex; flex-direction:column; gap:0.2rem; padding:0.6rem 0; border-bottom:1px solid var(--border); }
.cart-item-row { display:flex; align-items:center; }
.cart-item-name { flex:1; font-size:0.82rem; font-weight:600; }
.cart-item-remove { background:none; border:none; color:var(--accent7); cursor:pointer; font-size:0.9rem; padding:0.2rem 0.4rem; }
.cart-empty { padding:2rem; text-align:center; color:var(--text-dim); font-size:0.9rem; }
/* Configure Panel (Step 3) */
.config-layout { display:grid; grid-template-columns:1fr 340px; gap:2rem; }
@media(max-width:900px) { .config-layout { grid-template-columns:1fr; } }
.config-cart-list { display:flex; flex-direction:column; gap:0.5rem; }
.config-item { background:var(--surface); border:1px solid var(--border); border-radius:0.75rem; padding:1rem; display:flex; align-items:center; gap:0.75rem; }
.config-item-name { font-weight:600; font-size:0.88rem; flex:1; }
.config-item-cost { font-size:0.82rem; color:var(--accent4); font-weight:600; }
.config-item-remove { background:none; border:none; color:var(--accent7); cursor:pointer; font-size:1.1rem; }
.config-sidebar { position:sticky; top:80px; align-self:start; }
.param-group { background:var(--surface); border:1px solid var(--border); border-radius:0.75rem; padding:1.25rem; margin-bottom:1rem; }
.param-group h4 { font-size:0.85rem; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.75rem; }
.param-input { display:flex; flex-direction:column; gap:0.3rem; margin-bottom:0.75rem; }
.param-input label { font-size:0.82rem; color:var(--text-dim); }
.param-input input, .param-input select { padding:0.5rem; background:var(--surface2); border:1px solid var(--border); color:var(--text); border-radius:0.4rem; font-size:0.85rem; }
.compliance-list { display:flex; flex-direction:column; gap:0.5rem; }
.compliance-item { display:flex; align-items:center; gap:0.6rem; font-size:0.85rem; }
.traffic-light { width:12px; height:12px; border-radius:50%; flex-shrink:0; }
.tl-green { background:#66bb6a; }
.tl-yellow { background:#ffa726; }
.tl-red { background:#ef5350; }
.tl-gray { background:#555; }
/* Review Dashboard (Step 4) */
.review-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(320px, 1fr)); gap:1.5rem; }
.review-card { background:var(--surface); border:1px solid var(--border); border-radius:1rem; padding:1.5rem; }
.review-card h4 { font-size:0.85rem; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:1rem; }
.review-stat { font-size:2rem; font-weight:800; margin-bottom:0.5rem; }
.review-stat-label { font-size:0.82rem; color:var(--text-dim); }
.review-list { list-style:none; }
.review-list li { padding:0.4rem 0; border-bottom:1px solid var(--border); font-size:0.85rem; display:flex; align-items:center; gap:0.5rem; }
.review-list li:last-child { border-bottom:none; }
.cost-range-bar { display:flex; align-items:center; gap:0.5rem; margin:0.5rem 0; }
.cost-range-bar .label { font-size:0.75rem; color:var(--text-dim); width:30px; }
.cost-range-bar .bar { flex:1; height:8px; background:var(--surface2); border-radius:4px; overflow:hidden; }
.cost-range-bar .bar-fill { height:100%; border-radius:4px; }
.cost-range-bar .value { font-size:0.82rem; font-weight:600; width:100px; text-align:right; }
/* Export Section (Step 5) */
.export-tabs { display:flex; gap:0.5rem; margin-bottom:1.5rem; flex-wrap:wrap; }
.export-tab { padding:0.6rem 1.2rem; border-radius:2rem; border:1px solid var(--border); background:transparent; color:var(--text-dim); cursor:pointer; font-size:0.85rem; font-weight:500; transition:all 0.3s; }
.export-tab:hover, .export-tab.active { background:var(--accent); color:#fff; border-color:var(--accent); }
.export-pane { display:none; }
.export-pane.active { display:block; }
.export-output { background:var(--surface); border:1px solid var(--border); border-radius:0.75rem; padding:1.25rem; font-family:'SF Mono','Fira Code',monospace; font-size:0.8rem; line-height:1.6; color:var(--text); max-height:500px; overflow-y:auto; white-space:pre-wrap; }
.export-actions { display:flex; gap:0.5rem; margin-top:1rem; }
.export-actions button { padding:0.6rem 1.5rem; border:none; border-radius:0.5rem; font-weight:600; cursor:pointer; font-size:0.82rem; transition:all 0.2s; }
.btn-copy { background:var(--gradient1); color:#fff; }
.btn-copy:hover { opacity:0.9; }
.btn-download { background:var(--surface2); color:var(--text); border:1px solid var(--border) !important; }
.btn-download:hover { border-color:var(--accent) !important; }
.cmd-list { display:flex; flex-direction:column; gap:0.75rem; }
.cmd-row { background:var(--surface); border:1px solid var(--border); border-radius:0.75rem; padding:1rem; display:flex; align-items:center; gap:1rem; }
.cmd-num { width:28px; height:28px; border-radius:50%; background:var(--accent); color:#fff; display:flex; align-items:center; justify-content:center; font-size:0.8rem; font-weight:700; flex-shrink:0; }
.cmd-info { flex:1; }
.cmd-code { font-family:'SF Mono','Fira Code',monospace; font-size:0.82rem; color:var(--accent2); }
.cmd-desc { font-size:0.75rem; color:var(--text-dim); margin-top:0.2rem; }
.cmd-copy { background:none; border:1px solid var(--border); color:var(--text-dim); padding:0.3rem 0.6rem; border-radius:0.3rem; cursor:pointer; font-size:0.72rem; flex-shrink:0; }
.cmd-copy:hover { border-color:var(--accent); color:var(--accent); }
/* Step sections */
.step-section { padding:2rem 0; }
.step-section .section-title { text-align:center; margin-bottom:0.5rem; }
.step-section .section-subtitle { text-align:center; color:var(--text-dim); margin-bottom:2rem; font-size:1rem; }
.step-nav { display:flex; justify-content:space-between; margin-top:2rem; gap:1rem; }
.step-nav button { padding:0.7rem 2rem; border-radius:0.5rem; border:none; font-weight:600; font-size:0.9rem; cursor:pointer; transition:all 0.2s; }
/* Hidden */
.hidden { display:none !important; }
/* Scrollbar */
::-webkit-scrollbar { width:6px; height:6px; }
::-webkit-scrollbar-track { background:var(--bg); }
::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; }
::-webkit-scrollbar-thumb:hover { background:var(--text-dim); }
/* Responsive */
@media(max-width:768px) {
h1 { font-size:2rem; }
.scenarios-grid { grid-template-columns:1fr; }
.items-grid { grid-template-columns:1fr; }
.hero .stats { gap:1.5rem; }
.hero-modes { flex-direction:column; align-items:center; }
.stepper { overflow-x:auto; justify-content:flex-start; }
.wizard-inputs { grid-template-columns:1fr; }
.config-layout { grid-template-columns:1fr; }
.review-grid { grid-template-columns:1fr; }
}
</style>
</head>
<body>
<!-- HERO -->
<section class="hero" id="heroSection">
<h1>Azure AI Architecture Playground</h1>
<p>Fra problem til handlingsplan. Velg modus, bygg arkitekturen din, og eksporter en komplett command-pipeline for Claude.</p>
<div class="stats">
<div class="stat"><div class="stat-num">11</div><div class="stat-label">Azure AI-tjenester</div></div>
<div class="stat"><div class="stat-num">8</div><div class="stat-label">Kategorier</div></div>
<div class="stat"><div class="stat-num" id="totalItems">0</div><div class="stat-label">Kapabiliteter</div></div>
<div class="stat"><div class="stat-num">5</div><div class="stat-label">Pipeline-steg</div></div>
</div>
<div class="hero-modes">
<button class="hero-mode-btn" onclick="startGuided()">Guide meg</button>
<button class="hero-mode-btn" onclick="startExplore()">La meg utforske</button>
<button class="hero-mode-btn" onclick="startExpert()">Jeg vet hva jeg vil</button>
</div>
</section>
<!-- STEPPER -->
<nav class="stepper hidden" id="stepper">
<div class="step active" data-step="1" onclick="goToStep(1)"><div class="step-circle">1</div><div class="step-label">Intake</div></div>
<div class="step" data-step="2" onclick="goToStep(2)"><div class="step-circle">2</div><div class="step-label">Utforsk</div></div>
<div class="step" data-step="3" onclick="goToStep(3)"><div class="step-circle">3</div><div class="step-label">Konfigurer</div></div>
<div class="step" data-step="4" onclick="goToStep(4)"><div class="step-circle">4</div><div class="step-label">Gjennomgang</div></div>
<div class="step" data-step="5" onclick="goToStep(5)"><div class="step-circle">5</div><div class="step-label">Eksporter</div></div>
</nav>
<!-- STEP 1: INTAKE -->
<section id="step-1" class="container step-section hidden">
<div class="wizard" id="wizard">
<!-- Slide 1: Org Type -->
<div class="wizard-slide active" data-slide="1">
<h2>Hva slags organisasjon er du?</h2>
<p class="wizard-subtitle">Dette styrer compliance-krav og lisensanbefalinger.</p>
<div class="wizard-grid">
<div class="wizard-card" onclick="setOrgType(this,'statlig-etat')"><div class="card-icon">&#x1F3DB;</div>Statlig etat<div class="card-desc">Departement, direktorat, tilsyn</div></div>
<div class="wizard-card" onclick="setOrgType(this,'kommune')"><div class="card-icon">&#x1F3E0;</div>Kommune<div class="card-desc">Kommune eller bydel</div></div>
<div class="wizard-card" onclick="setOrgType(this,'fylkeskommune')"><div class="card-icon">&#x1F5FA;</div>Fylkeskommune<div class="card-desc">Regional forvaltning</div></div>
<div class="wizard-card" onclick="setOrgType(this,'universitet')"><div class="card-icon">&#x1F393;</div>Universitet/hogskole<div class="card-desc">UH-sektor</div></div>
<div class="wizard-card" onclick="setOrgType(this,'helsevesen')"><div class="card-icon">&#x1F3E5;</div>Helsevesen<div class="card-desc">Sykehus, helseforetak</div></div>
<div class="wizard-card" onclick="setOrgType(this,'privat')"><div class="card-icon">&#x1F3E2;</div>Privat virksomhet<div class="card-desc">Naringsliv</div></div>
</div>
<div class="wizard-nav"><span></span><button class="wizard-next" onclick="nextSlide()">Neste &#x2192;</button></div>
</div>
<!-- Slide 2: Org Size -->
<div class="wizard-slide" data-slide="2">
<h2>Hvor stor er organisasjonen?</h2>
<p class="wizard-subtitle">Pavirker lisenskostnad og skaleringsanbefalinger.</p>
<div class="wizard-grid">
<div class="wizard-card" onclick="setSize(this,'1-50')"><div class="card-icon">&#x1F464;</div>1-50 ansatte</div>
<div class="wizard-card" onclick="setSize(this,'50-200')"><div class="card-icon">&#x1F465;</div>50-200 ansatte</div>
<div class="wizard-card" onclick="setSize(this,'200-1000')"><div class="card-icon">&#x1F46B;</div>200-1000 ansatte</div>
<div class="wizard-card" onclick="setSize(this,'1000-5000')"><div class="card-icon">&#x1F46A;</div>1000-5000 ansatte</div>
<div class="wizard-card" onclick="setSize(this,'5000+')"><div class="card-icon">&#x1F30D;</div>5000+ ansatte</div>
</div>
<div class="wizard-nav"><button class="wizard-prev" onclick="prevSlide()">&#x2190; Tilbake</button><button class="wizard-next" onclick="nextSlide()">Neste &#x2192;</button></div>
</div>
<!-- Slide 3: Licenses -->
<div class="wizard-slide" data-slide="3">
<h2>Hvilke lisenser har dere?</h2>
<p class="wizard-subtitle">Velg alle som gjelder. Styrer hvilke kapabiliteter som er tilgjengelige.</p>
<div class="wizard-checks">
<label class="wizard-check" onclick="toggleCheck(this)"><input type="checkbox" value="m365-e3" onchange="updateIntakeLicenses()"> Microsoft 365 E3</label>
<label class="wizard-check" onclick="toggleCheck(this)"><input type="checkbox" value="m365-e5" onchange="updateIntakeLicenses()"> Microsoft 365 E5</label>
<label class="wizard-check" onclick="toggleCheck(this)"><input type="checkbox" value="copilot-license" onchange="updateIntakeLicenses()"> M365 Copilot add-on</label>
<label class="wizard-check" onclick="toggleCheck(this)"><input type="checkbox" value="copilot-studio" onchange="updateIntakeLicenses()"> Copilot Studio</label>
<label class="wizard-check" onclick="toggleCheck(this)"><input type="checkbox" value="azure-payg" onchange="updateIntakeLicenses()"> Azure Pay-as-you-go</label>
<label class="wizard-check" onclick="toggleCheck(this)"><input type="checkbox" value="power-premium" onchange="updateIntakeLicenses()"> Power Platform Premium</label>
</div>
<div class="wizard-nav"><button class="wizard-prev" onclick="prevSlide()">&#x2190; Tilbake</button><button class="wizard-next" onclick="nextSlide()">Neste &#x2192;</button></div>
</div>
<!-- Slide 4: Compliance + Residency -->
<div class="wizard-slide" data-slide="4">
<h2>Compliance og dataresidens</h2>
<p class="wizard-subtitle">Viktige krav som filtrerer hvilke tjenester som er aktuelle.</p>
<h3 style="margin-bottom:0.75rem">Compliance-krav</h3>
<div class="wizard-checks" style="margin-bottom:1.5rem">
<label class="wizard-check" onclick="toggleCheck(this)"><input type="checkbox" value="schrems-ii" onchange="updateIntakeCompliance()"> Schrems II-safe (kun EU/Norge)</label>
<label class="wizard-check" onclick="toggleCheck(this)"><input type="checkbox" value="dpia-required" onchange="updateIntakeCompliance()"> DPIA/PVK pakrevd</label>
<label class="wizard-check" onclick="toggleCheck(this)"><input type="checkbox" value="ai-act-high" onchange="updateIntakeCompliance()"> AI Act hoy-risiko</label>
<label class="wizard-check" onclick="toggleCheck(this)"><input type="checkbox" value="nsm-grunnprinsipper" onchange="updateIntakeCompliance()"> NSM Grunnprinsipper</label>
</div>
<h3 style="margin-bottom:0.75rem">EU AI Act-klassifisering</h3>
<div class="wizard-inputs" style="margin-bottom:1.5rem">
<div class="wizard-field">
<label>Risikoniva</label>
<select onchange="updateAiActLevel(this.value)">
<option value="">Ikke vurdert</option>
<option value="minimal">Minimal risiko</option>
<option value="limited">Begrenset risiko</option>
<option value="high-risk">Hoyrisiko (Annex III)</option>
<option value="prohibited">Forbudt (Art. 5)</option>
</select>
</div>
<div class="wizard-field">
<label>Rolle i AI Act</label>
<select onchange="state.intake.aiActRole=this.value">
<option value="">Ikke bestemt</option>
<option value="deployer">Deployer</option>
<option value="provider">Provider</option>
<option value="deployer-provider">Deployer + Provider</option>
</select>
</div>
</div>
<h3 style="margin-bottom:0.75rem">Krav til dataresidens</h3>
<div class="wizard-radios">
<label class="wizard-radio"><input type="radio" name="residency" value="norway" onchange="updateIntakeResidency()"> Norway East (strengest)</label>
<label class="wizard-radio"><input type="radio" name="residency" value="eu" checked onchange="updateIntakeResidency()"> EU Data Boundary (standard)</label>
<label class="wizard-radio"><input type="radio" name="residency" value="global" onchange="updateIntakeResidency()"> Global (ingen krav)</label>
</div>
<div class="wizard-nav"><button class="wizard-prev" onclick="prevSlide()">&#x2190; Tilbake</button><button class="wizard-next" onclick="nextSlide()">Neste &#x2192;</button></div>
</div>
<!-- Slide 5: Scenario -->
<div class="wizard-slide" data-slide="5">
<h2>Hva skal du bygge?</h2>
<p class="wizard-subtitle">Velg et scenario for a fa en anbefalt handlekurv, eller skriv fritekst.</p>
<div class="scenarios-grid" id="wizardScenarios"></div>
<div style="margin-top:1.5rem">
<div class="wizard-field">
<label>Eller beskriv problemet ditt:</label>
<textarea id="intakeFreetext" placeholder="F.eks.: Vi trenger en chatbot som kan svare pa sporsmaal om interne retningslinjer..." oninput="state.intake.freetext=this.value"></textarea>
</div>
</div>
<div class="wizard-nav"><button class="wizard-prev" onclick="prevSlide()">&#x2190; Tilbake</button><button class="wizard-next" onclick="nextSlide()">Neste &#x2192;</button></div>
</div>
<!-- Slide 6: Details -->
<div class="wizard-slide" data-slide="6">
<h2>Siste detaljer</h2>
<p class="wizard-subtitle">Valgfritt — disse forbedrer kostnadsestimat og command-argumenter.</p>
<div class="wizard-inputs">
<div class="wizard-field">
<label>Antall brukere</label>
<select id="intakeUsers" onchange="state.intake.users=this.value">
<option value="">Velg...</option>
<option value="1-50">1-50</option>
<option value="50-200">50-200</option>
<option value="200-1000">200-1000</option>
<option value="1000-5000">1000-5000</option>
<option value="5000+">5000+</option>
</select>
</div>
<div class="wizard-field">
<label>Estimert volum per dag</label>
<input type="text" id="intakeVolume" placeholder="F.eks.: 500 meldinger" oninput="state.intake.volume=this.value">
</div>
<div class="wizard-field">
<label>Budsjettramme (NOK/mnd)</label>
<input type="number" id="intakeBudget" placeholder="F.eks.: 50000" oninput="state.intake.budget=this.value">
</div>
<div class="wizard-field">
<label>Tidsramme</label>
<select id="intakeTimeframe" onchange="state.intake.timeframe=this.value">
<option value="">Velg...</option>
<option value="poc-4uker">POC (4 uker)</option>
<option value="pilot-3mnd">Pilot (3 maneder)</option>
<option value="produksjon-6mnd">Produksjon (6 maneder)</option>
<option value="enterprise-12mnd">Enterprise (12+ maneder)</option>
</select>
</div>
<div class="wizard-field" style="grid-column:1/-1">
<label>Malgruppe</label>
<select id="intakeTarget" onchange="state.intake.target=this.value">
<option value="">Velg...</option>
<option value="interne-ansatte">Interne ansatte</option>
<option value="innbyggere">Innbyggere/kunder</option>
<option value="studenter">Studenter</option>
<option value="bade">Bade interne og eksterne</option>
</select>
</div>
</div>
<div class="wizard-nav"><button class="wizard-prev" onclick="prevSlide()">&#x2190; Tilbake</button><button class="wizard-next" onclick="submitWizard()">Fullfar intake &#x2192;</button></div>
</div>
</div>
</section>
<!-- STEP 2: EXPLORE -->
<section id="step-2" class="container step-section hidden">
<div class="main-layout">
<aside class="sidebar">
<div class="filter-group">
<h4>Lisenstype</h4>
<label class="filter-option"><input type="checkbox" checked onchange="applyFilters()" data-filter="license" value="m365-e3"> M365 E3</label>
<label class="filter-option"><input type="checkbox" checked onchange="applyFilters()" data-filter="license" value="m365-e5"> M365 E5</label>
<label class="filter-option"><input type="checkbox" checked onchange="applyFilters()" data-filter="license" value="copilot-license"> M365 Copilot add-on</label>
<label class="filter-option"><input type="checkbox" checked onchange="applyFilters()" data-filter="license" value="copilot-studio"> Copilot Studio</label>
<label class="filter-option"><input type="checkbox" checked onchange="applyFilters()" data-filter="license" value="azure-payg"> Azure Pay-as-you-go</label>
<label class="filter-option"><input type="checkbox" checked onchange="applyFilters()" data-filter="license" value="power-premium"> Power Platform Premium</label>
</div>
<div class="filter-group">
<h4>Dataresidens</h4>
<label class="filter-option"><input type="checkbox" checked onchange="applyFilters()" data-filter="residency" value="norway"> Norway East</label>
<label class="filter-option"><input type="checkbox" checked onchange="applyFilters()" data-filter="residency" value="eu"> EU Data Boundary</label>
<label class="filter-option"><input type="checkbox" checked onchange="applyFilters()" data-filter="residency" value="global"> Global</label>
</div>
<div class="filter-group">
<h4>Compliance</h4>
<label class="filter-option"><input type="checkbox" onchange="applyFilters()" data-filter="compliance" value="schrems-ii"> Kun Schrems II-safe</label>
<label class="filter-option"><input type="checkbox" onchange="applyFilters()" data-filter="compliance" value="dpia-required"> Vis DPIA-krav</label>
<label class="filter-option"><input type="checkbox" onchange="applyFilters()" data-filter="compliance" value="ai-act-high"> AI Act hoy-risiko</label>
</div>
<div class="filter-group">
<h4>Modenhet</h4>
<label class="filter-option"><input type="checkbox" checked onchange="applyFilters()" data-filter="maturity" value="ga"> GA (Generally Available)</label>
<label class="filter-option"><input type="checkbox" checked onchange="applyFilters()" data-filter="maturity" value="preview"> Preview</label>
</div>
<div class="filter-group">
<h4>Kostnadsniva</h4>
<label class="filter-option"><input type="checkbox" checked onchange="applyFilters()" data-filter="cost" value="free"> Gratis / Inkludert</label>
<label class="filter-option"><input type="checkbox" checked onchange="applyFilters()" data-filter="cost" value="low"> Lav (&lt;$100/mnd)</label>
<label class="filter-option"><input type="checkbox" checked onchange="applyFilters()" data-filter="cost" value="medium"> Medium ($100-1000/mnd)</label>
<label class="filter-option"><input type="checkbox" checked onchange="applyFilters()" data-filter="cost" value="high"> Hoy (&gt;$1000/mnd)</label>
</div>
<button class="filter-reset" onclick="resetFilters()">Nullstill alle filtre</button>
</aside>
<div class="browse-content">
<div class="search-bar">
<input type="text" class="search-input" placeholder="Sok i kapabiliteter..." oninput="searchItems(this.value)">
</div>
<div id="aislesContainer"></div>
</div>
</div>
<div class="step-nav">
<button class="wizard-prev" onclick="goToStep(1)">&#x2190; Tilbake til intake</button>
<button class="wizard-next" onclick="goToStep(3)">Konfigurer &#x2192;</button>
</div>
</section>
<!-- STEP 3: CONFIGURE -->
<section id="step-3" class="container step-section hidden">
<h2 class="section-title">Konfigurer arkitekturen</h2>
<p class="section-subtitle">Juster parametere og sjekk compliance-status for dine valg.</p>
<div id="configureContent"></div>
<div class="step-nav">
<button class="wizard-prev" onclick="goToStep(2)">&#x2190; Tilbake</button>
<button class="wizard-next" onclick="goToStep(4)">Gjennomgang &#x2192;</button>
</div>
</section>
<!-- STEP 4: REVIEW -->
<section id="step-4" class="container step-section hidden">
<h2 class="section-title">Gjennomgang</h2>
<p class="section-subtitle">Oversikt over arkitekturen, kostnader, compliance og risiko.</p>
<div class="review-grid" id="reviewContent"></div>
<div class="step-nav">
<button class="wizard-prev" onclick="goToStep(3)">&#x2190; Tilbake</button>
<button class="wizard-next" onclick="goToStep(5)">Eksporter &#x2192;</button>
</div>
</section>
<!-- STEP 5: EXPORT -->
<section id="step-5" class="container step-section hidden">
<h2 class="section-title">Eksporter</h2>
<p class="section-subtitle">Velg format og ta med deg arkitekturbeslutningen videre.</p>
<div class="export-tabs">
<button class="export-tab active" onclick="showExportTab('prompt')">Prompt</button>
<button class="export-tab" onclick="showExportTab('pipeline')">Command Pipeline</button>
<button class="export-tab" onclick="showExportTab('brief')">Brief (.md)</button>
<button class="export-tab" onclick="showExportTab('json')">JSON Record</button>
</div>
<div id="exportContent"></div>
<div class="step-nav">
<button class="wizard-prev" onclick="goToStep(4)">&#x2190; Tilbake</button>
<button class="wizard-next" onclick="startOver()">Start pa nytt</button>
</div>
</section>
<!-- CART FAB -->
<div class="cart-fab" id="cartFab">
<button class="cart-btn" onclick="toggleCart()">&#x1F6D2;<span class="cart-badge" id="cartCount">0</span></button>
</div>
<!-- CART PANEL -->
<div class="cart-panel" id="cartPanel">
<div class="cart-header">
<h3>Handlekurv</h3>
<button class="cart-close" onclick="toggleCart()">&times;</button>
</div>
<div class="cart-body" id="cartItems">
<div class="cart-empty">Klikk pa kapabiliteter for a legge dem i handlekurven.</div>
</div>
</div>
<script>
// =========================================================================
// DATA MODEL — BRANDS, AISLES, ITEMS
// =========================================================================
const BRANDS = {
'm365': { name: 'M365 Copilot', color: 'source-m365', short: 'M365' },
'studio': { name: 'Copilot Studio', color: 'source-studio', short: 'Studio' },
'foundry': { name: 'Azure AI Foundry', color: 'source-foundry', short: 'Foundry' },
'openai': { name: 'Azure OpenAI Service', color: 'source-openai', short: 'OpenAI' },
'search': { name: 'Azure AI Search', color: 'source-search', short: 'Search' },
'services': { name: 'Azure AI Services', color: 'source-services', short: 'Services' },
'sk': { name: 'Semantic Kernel', color: 'source-sk', short: 'SK' },
'power': { name: 'Power Platform AI', color: 'source-power', short: 'Power' },
'graph': { name: 'Microsoft Graph', color: 'source-graph', short: 'Graph' }
};
const AISLES = [
{ id: 'llm', name: 'LLM-tilgang', icon: '\u{1F916}', desc: 'Modellvalg, deployment og tilgangsmonstre' },
{ id: 'rag', name: 'RAG & Sok', icon: '\u{1F50D}', desc: 'Vektorindeks, hybrid search, grounding' },
{ id: 'agent', name: 'Agent-orkestrering', icon: '\u{1F3AF}', desc: 'Multi-agent, tool use, autonomi' },
{ id: 'identity', name: 'Identitet & Auth', icon: '\u{1F512}', desc: 'Managed Identity, RBAC, Entra ID' },
{ id: 'security', name: 'Sikkerhet & Compliance', icon: '\u{1F6E1}', desc: 'Content Safety, DLP, Schrems II' },
{ id: 'channels', name: 'Kanaler & UX', icon: '\u{1F4AC}', desc: 'Teams, web, WhatsApp, Adaptive Cards' },
{ id: 'data', name: 'Data & Integrasjon', icon: '\u{1F517}', desc: 'Graph, connectors, datakilder' },
{ id: 'observability', name: 'Observability', icon: '\u{1F4CA}', desc: 'Logging, monitoring, evaluering' }
];
const ITEMS = [
// === LLM-tilgang ===
{ id:'llm-gpt4o', name:'GPT-4o (Standard deployment)', desc:'Flaggskipmodell for generell bruk. Balanse mellom kvalitet og kostnad. Multimodal (tekst + bilde).', aisle:'llm', sources:['openai','foundry'], cost:'medium', costEst:80, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:['rag-chatbot','autonomous-agent','multi-agent','intelligent-search'], skill:'pro', setupDays:3, userRec:null },
{ id:'llm-gpt4o-mini', name:'GPT-4o-mini (Kostnadseffektiv)', desc:'94% billigere enn GPT-4o. Ideell for enkle oppgaver, klassifisering, og hoyvolum-bruk.', aisle:'llm', sources:['openai','foundry'], cost:'low', costEst:5, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:['document-classification','customer-service','reporting'], skill:'pro', setupDays:3, userRec:null },
{ id:'llm-o1', name:'o1/o3 Reasoning-modeller', desc:'Avansert resonnering for komplekse problemer. Hoyre kostnad, men bedre pa analyse og logikk.', aisle:'llm', sources:['openai','foundry'], cost:'high', costEst:200, license:['azure-payg'], residency:['eu','global'], maturity:'ga', scenarios:['autonomous-agent','multi-agent'], skill:'pro', setupDays:3, userRec:null },
{ id:'llm-ptu', name:'Provisioned Throughput (PTU)', desc:'Reservert kapasitet med forutsigbar kostnad. Anbefales ved >100K requests/maned.', aisle:'llm', sources:['openai'], cost:'high', costEst:2000, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:[], skill:'devops', setupDays:7, userRec:'Anbefalt ved >100K requests/mnd' },
{ id:'llm-router', name:'Model Router (kostnadsoptimering)', desc:'Automatisk routing til billigste modell som moter kvalitetskrav. Ingen ekstra kostnad.', aisle:'llm', sources:['foundry'], cost:'free', costEst:0, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:['multi-agent'], skill:'pro', setupDays:1, userRec:null },
{ id:'llm-catalog', name:'Model Catalog (1000+ modeller)', desc:'Tilgang til open-source modeller fra Meta, Mistral, Hugging Face m.fl. via Azure AI Foundry.', aisle:'llm', sources:['foundry'], cost:'medium', costEst:100, license:['azure-payg'], residency:['eu','global'], maturity:'ga', scenarios:[], skill:'devops', setupDays:5, userRec:null },
{ id:'llm-studio-gen', name:'Copilot Studio generativ orkestrering', desc:'Innebygd LLM-tilgang i Copilot Studio. Ingen separat Azure OpenAI-oppsett nodvendig.', aisle:'llm', sources:['studio'], cost:'medium', costEst:200, license:['copilot-studio'], residency:['norway','eu'], maturity:'ga', scenarios:['customer-service','copilot-extension'], skill:'citizen', setupDays:1, userRec:null },
{ id:'llm-m365', name:'M365 Copilot (managed GPT)', desc:'Ferdigkonfigurert GPT i Word, Excel, PowerPoint, Outlook, Teams. Ingen modellvalg nodvendig.', aisle:'llm', sources:['m365'], cost:'medium', costEst:30, license:['copilot-license','m365-e3','m365-e5'], residency:['norway','eu'], maturity:'ga', scenarios:['copilot-extension','reporting'], skill:'citizen', setupDays:0, userRec:null },
{ id:'llm-finetune', name:'Fine-tuned modell-deployment', desc:'Tilpass GPT-modeller med egne data. Hosting koster 24/7 — slett ubrukte deployments.', aisle:'llm', sources:['openai','foundry'], cost:'high', costEst:3000, license:['azure-payg'], residency:['eu','global'], maturity:'ga', scenarios:[], skill:'devops', setupDays:14, userRec:null },
{ id:'llm-gpt5', name:'GPT-5 (Flaggskipmodell)', desc:'Neste generasjon flaggskipmodell. Styrket resonnering, multimodal, 1M context window. Registreringskrav for tilgang.', aisle:'llm', sources:['openai','foundry'], cost:'high', costEst:300, license:['azure-payg'], residency:['eu','global'], maturity:'ga', scenarios:['autonomous-agent','multi-agent','intelligent-search'], skill:'pro', setupDays:3, userRec:'Registreringskrav — verifiser tilgang' },
{ id:'llm-gpt5-mini', name:'GPT-5-mini (Kostnadsoptimert)', desc:'Kompakt GPT-5 for hoyvolum-bruk. Bedre enn GPT-4o til lavere kostnad. God balanse for de fleste oppgaver.', aisle:'llm', sources:['openai','foundry'], cost:'medium', costEst:50, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:['rag-chatbot','customer-service','document-classification','reporting'], skill:'pro', setupDays:3, userRec:null },
{ id:'llm-gpt41', name:'GPT-4.1 (Kode og instruksjoner)', desc:'Optimert for kode, instruksjonsfolging og lange kontekster. 1M context window. Erstatter GPT-4o.', aisle:'llm', sources:['openai','foundry'], cost:'medium', costEst:60, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:['rag-chatbot','autonomous-agent','intelligent-search'], skill:'pro', setupDays:3, userRec:null },
{ id:'llm-gpt41-mini', name:'GPT-4.1-mini (Hurtig og billig)', desc:'94% billigere enn GPT-4.1. Ideell for klassifisering, enkel Q&A og hoyvolum. 1M context window.', aisle:'llm', sources:['openai','foundry'], cost:'low', costEst:5, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:['document-classification','customer-service','reporting'], skill:'pro', setupDays:3, userRec:null },
{ id:'llm-deepseek', name:'DeepSeek-R1/V3 (Open Source)', desc:'Kostnadseffektive reasoning-modeller fra DeepSeek. Tilgjengelig via Foundry Models-as-a-Service.', aisle:'llm', sources:['foundry'], cost:'low', costEst:15, license:['azure-payg'], residency:['eu','global'], maturity:'ga', scenarios:['rag-chatbot'], skill:'devops', setupDays:5, userRec:null },
{ id:'llm-copilot-tuning', name:'Copilot Tuning (Enterprise Fine-tune)', desc:'Fine-tune M365 Copilot pa enterprise-data. Tilpass terminologi og domeneviten. Krever 5000+ Copilot-lisenser.', aisle:'llm', sources:['m365'], cost:'high', costEst:0, license:['copilot-license'], residency:['eu'], maturity:'preview', scenarios:['copilot-extension'], skill:'devops', setupDays:30, userRec:'Krever 5000+ M365 Copilot-lisenser' },
// === RAG & Sok ===
{ id:'rag-hybrid', name:'Azure AI Search — Hybrid Vector+Keyword', desc:'Kombinerer semantisk og keyword-sok for optimal recall. Beste generelle RAG-losning.', aisle:'rag', sources:['search'], cost:'medium', costEst:250, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:['rag-chatbot','intelligent-search','autonomous-agent'], skill:'pro', setupDays:5, userRec:null },
{ id:'rag-integrated', name:'Azure AI Search — Integrated Vectorization', desc:'Automatisk chunking og vektorisering. Minimal kode, rask oppsett for standard dokumenter.', aisle:'rag', sources:['search'], cost:'medium', costEst:250, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:['rag-chatbot','intelligent-search'], skill:'pro', setupDays:3, userRec:null },
{ id:'rag-custom-skill', name:'Azure AI Search — Custom Skillset', desc:'Avansert chunking med egne pipelines. For komplekse dokumentformater eller spesielle krav.', aisle:'rag', sources:['search'], cost:'medium', costEst:300, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:[], skill:'devops', setupDays:10, userRec:null },
{ id:'rag-graph-connector', name:'SharePoint Graph Connector', desc:'Indekser SharePoint, OneDrive og Teams-innhold direkte i Azure AI Search.', aisle:'rag', sources:['search','graph'], cost:'low', costEst:10, license:['azure-payg','m365-e3','m365-e5'], residency:['norway','eu'], maturity:'ga', scenarios:['rag-chatbot','intelligent-search'], skill:'citizen', setupDays:2, userRec:null },
{ id:'rag-tenant-grounding', name:'Copilot Studio Tenant Graph Grounding', desc:'RAG over Microsoft Graph-data direkte i Copilot Studio. 10 Copilot Credits per melding.', aisle:'rag', sources:['studio','graph'], cost:'medium', costEst:100, license:['copilot-studio'], residency:['norway','eu'], maturity:'ga', scenarios:['customer-service','copilot-extension'], skill:'citizen', setupDays:1, userRec:null },
{ id:'rag-m365-retrieval', name:'M365 Retrieval API', desc:'Programmatisk tilgang til M365-data for custom RAG-apper. Krever M365 Copilot-lisens.', aisle:'rag', sources:['m365','graph'], cost:'low', costEst:30, license:['copilot-license'], residency:['norway','eu'], maturity:'ga', scenarios:['copilot-extension'], skill:'pro', setupDays:3, userRec:null },
{ id:'rag-agentic', name:'Agentic Retrieval', desc:'AI-drevet sok som itererer og forfiner sporsmal automatisk. Preview-funksjon.', aisle:'rag', sources:['search','foundry'], cost:'medium', costEst:300, license:['azure-payg'], residency:['eu','global'], maturity:'preview', scenarios:['multi-agent','autonomous-agent'], skill:'devops', setupDays:7, userRec:null },
{ id:'rag-cosmos', name:'Cosmos DB Vector Search', desc:'Vektorsok integrert i Cosmos DB. Bra nar data allerede er i Cosmos.', aisle:'rag', sources:['foundry'], cost:'medium', costEst:200, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:[], skill:'devops', setupDays:7, userRec:null },
// === Agent-orkestrering ===
{ id:'agent-builder', name:'M365 Copilot Agent Builder', desc:'Deklarer agenter som utvider M365 Copilot. No-code, bruker Graph-data. For enkle Q&A-agenter.', aisle:'agent', sources:['m365'], cost:'free', costEst:0, license:['copilot-license'], residency:['norway','eu'], maturity:'ga', scenarios:['copilot-extension'], skill:'citizen', setupDays:1, userRec:null },
{ id:'agent-studio-topics', name:'Copilot Studio Topics & Triggers', desc:'Regelbasert og generativ orkestrering. Low-code med visuell designer.', aisle:'agent', sources:['studio'], cost:'medium', costEst:200, license:['copilot-studio'], residency:['norway','eu'], maturity:'ga', scenarios:['customer-service','autonomous-agent'], skill:'citizen', setupDays:2, userRec:null },
{ id:'agent-studio-auto', name:'Copilot Studio Autonomous Agents', desc:'Agenter som kjorer i bakgrunnen, trigget av hendelser i M365. Ingen brukerinteraksjon.', aisle:'agent', sources:['studio'], cost:'medium', costEst:300, license:['copilot-studio'], residency:['norway','eu'], maturity:'ga', scenarios:['autonomous-agent'], skill:'pro', setupDays:3, userRec:null },
{ id:'agent-foundry', name:'Foundry Agent Service (code-first)', desc:'Full kontroll over agent-logikk. Code Interpreter, File Search, custom tools. For komplekse behov.', aisle:'agent', sources:['foundry'], cost:'high', costEst:500, license:['azure-payg'], residency:['eu','global'], maturity:'ga', scenarios:['multi-agent','autonomous-agent'], skill:'devops', setupDays:10, userRec:null },
{ id:'agent-sk', name:'Semantic Kernel orkestrering', desc:'Open-source SDK for agent-orkestrering i Python/.NET. Full kontroll over planlegging og tool use.', aisle:'agent', sources:['sk'], cost:'free', costEst:0, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:['multi-agent'], skill:'devops', setupDays:14, userRec:null },
{ id:'agent-mcp', name:'Multi-agent via Copilot Studio + MCP', desc:'Koble flere Copilot Studio-agenter sammen via Model Context Protocol. Moderat kompleksitet.', aisle:'agent', sources:['studio'], cost:'medium', costEst:400, license:['copilot-studio'], residency:['norway','eu'], maturity:'preview', scenarios:['multi-agent'], skill:'pro', setupDays:5, userRec:null },
{ id:'agent-power-automate', name:'Power Automate AI Flows', desc:'AI-drevne automatiserings-flyter. Kobler AI Builder med 1000+ connectors.', aisle:'agent', sources:['power'], cost:'low', costEst:40, license:['power-premium'], residency:['norway','eu'], maturity:'ga', scenarios:['document-classification','reporting'], skill:'citizen', setupDays:2, userRec:null },
{ id:'agent-cua', name:'Computer-Using Agents (CUA)', desc:'AI-agenter som styrer GUI via skjermbilder og museklikk. Desktop-automatisering uten API. Copilot Studio-integrasjon.', aisle:'agent', sources:['studio'], cost:'medium', costEst:200, license:['copilot-studio'], residency:['eu'], maturity:'preview', scenarios:['autonomous-agent'], skill:'pro', setupDays:7, userRec:'Preview — verifiser tilgjengelighet' },
{ id:'agent-workflows', name:'Foundry Workflows (visuell orkestrering)', desc:'Drag-and-drop designer for multi-agent workflows. Branching, feilhaandtering, parallell kjoring.', aisle:'agent', sources:['foundry'], cost:'medium', costEst:100, license:['azure-payg'], residency:['eu','global'], maturity:'ga', scenarios:['multi-agent','autonomous-agent'], skill:'pro', setupDays:7, userRec:null },
{ id:'agent-a2a', name:'Agent2Agent (A2A) protokoll', desc:'Aapen standard for agent-til-agent kommunikasjon. Interoperabilitet mellom Foundry, Copilot Studio og tredjepart.', aisle:'agent', sources:['foundry','studio'], cost:'free', costEst:0, license:['azure-payg','copilot-studio'], residency:['norway','eu','global'], maturity:'ga', scenarios:['multi-agent'], skill:'devops', setupDays:5, userRec:null },
{ id:'agent-deep-research', name:'Deep Research (o3 + Bing)', desc:'Dybdeanalyse med automatisk websok og kildehenvisninger. Bruker o3-deep-research-modellen.', aisle:'agent', sources:['foundry'], cost:'high', costEst:200, license:['azure-payg'], residency:['eu','global'], maturity:'ga', scenarios:['intelligent-search'], skill:'pro', setupDays:3, userRec:null },
// === Identitet & Auth ===
{ id:'auth-entra', name:'Microsoft Entra ID (SSO)', desc:'Single Sign-On for alle Azure og M365-tjenester. Grunnleggende identitetsplattform.', aisle:'identity', sources:['m365','foundry','studio'], cost:'free', costEst:0, license:['m365-e3','m365-e5'], residency:['norway','eu'], maturity:'ga', scenarios:['rag-chatbot','autonomous-agent','copilot-extension','customer-service','multi-agent','intelligent-search','document-classification','reporting'], skill:'citizen', setupDays:1, userRec:null },
{ id:'auth-managed-id', name:'Managed Identity for Azure-ressurser', desc:'Automatisk credential-haandtering mellom Azure-tjenester. Ingen secrets a administrere.', aisle:'identity', sources:['foundry','openai','search'], cost:'free', costEst:0, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:['rag-chatbot','multi-agent','autonomous-agent','intelligent-search'], skill:'pro', setupDays:2, userRec:null },
{ id:'auth-rbac', name:'RBAC (Role-Based Access Control)', desc:'Granulert tilgangsstyring per ressurs. AI User, AI Administrator, Cognitive Services User.', aisle:'identity', sources:['foundry','openai','search'], cost:'free', costEst:0, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:['rag-chatbot','multi-agent','autonomous-agent'], skill:'pro', setupDays:2, userRec:null },
{ id:'auth-conditional', name:'Conditional Access-policyer', desc:'Kontekstbasert tilgangskontroll: krev MFA, blokker fra utenfor Norge, krev compliant device.', aisle:'identity', sources:['m365'], cost:'free', costEst:0, license:['m365-e3','m365-e5'], residency:['norway','eu'], maturity:'ga', scenarios:['autonomous-agent','copilot-extension'], skill:'pro', setupDays:2, userRec:null },
{ id:'auth-mfa', name:'Multi-Factor Authentication (MFA)', desc:'Obligatorisk for norsk offentlig sektor. Entra ID MFA inkludert i alle M365-lisenser.', aisle:'identity', sources:['m365'], cost:'free', costEst:0, license:['m365-e3','m365-e5'], residency:['norway','eu'], maturity:'ga', scenarios:['rag-chatbot','copilot-extension','autonomous-agent'], skill:'citizen', setupDays:1, userRec:null },
{ id:'auth-lockbox', name:'Customer Lockbox', desc:'Krever eksplisitt godkjenning for Microsoft-tilgang til dine data. Viktig for Schrems II.', aisle:'identity', sources:['m365'], cost:'free', costEst:0, license:['m365-e5'], residency:['norway','eu'], maturity:'ga', scenarios:['autonomous-agent'], skill:'pro', setupDays:1, userRec:null },
{ id:'auth-agent-id', name:'Entra Agent ID (agentidentitet)', desc:'Identitetsstyring for AI-agenter. Zero Trust-prinsipper: verifiser, minste privilegium, anta brudd. Agent Registry for livssyklus.', aisle:'identity', sources:['m365','foundry','studio'], cost:'free', costEst:0, license:['m365-e5'], residency:['norway','eu'], maturity:'preview', scenarios:['autonomous-agent','multi-agent'], skill:'pro', setupDays:5, userRec:'Anbefalt for alle produksjonsagenter' },
// === Sikkerhet & Compliance ===
{ id:'sec-content-safety', name:'Azure AI Content Safety', desc:'Filtrerer skadelig innhold i AI-output. Konfigurerbare filtre for vold, hat, seksuelt innhold.', aisle:'security', sources:['foundry','openai'], cost:'low', costEst:10, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:['customer-service','rag-chatbot'], skill:'pro', setupDays:2, userRec:null },
{ id:'sec-prompt-shields', name:'Prompt Shields (injeksjonsforsvar)', desc:'Beskytter mot prompt injection-angrep. Kritisk for eksternt eksponerte agenter.', aisle:'security', sources:['foundry','openai','studio'], cost:'low', costEst:5, license:['azure-payg','copilot-studio'], residency:['norway','eu','global'], maturity:'ga', scenarios:['customer-service'], skill:'pro', setupDays:1, userRec:null },
{ id:'sec-purview-dlp', name:'Microsoft Purview DLP', desc:'Data Loss Prevention — hindrer at sensitiv data lekker via AI-svar. E3: basic, E5: advanced.', aisle:'security', sources:['m365'], cost:'free', costEst:0, license:['m365-e3','m365-e5'], residency:['norway','eu'], maturity:'ga', scenarios:['copilot-extension','rag-chatbot','autonomous-agent'], skill:'pro', setupDays:3, userRec:null },
{ id:'sec-sensitivity', name:'Sensitivity Labels', desc:'Klassifiser og beskytt dokumenter automatisk. AI respekterer labels i RAG-scenarioer.', aisle:'security', sources:['m365'], cost:'free', costEst:0, license:['m365-e3','m365-e5'], residency:['norway','eu'], maturity:'ga', scenarios:['rag-chatbot','copilot-extension'], skill:'pro', setupDays:3, userRec:null },
{ id:'sec-defender', name:'Defender for Cloud AI SPM', desc:'Security Posture Management for AI-arbeidsbelastninger. Oppdager feilkonfigurasjoner og trusler.', aisle:'security', sources:['foundry'], cost:'medium', costEst:100, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:[], skill:'devops', setupDays:5, userRec:null },
{ id:'sec-private', name:'Private Endpoints (nettverksisolering)', desc:'AI-tjenester kun tilgjengelig via privat nettverk. Ingen offentlig internett-eksponering.', aisle:'security', sources:['foundry','openai','search'], cost:'low', costEst:50, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:['multi-agent','autonomous-agent'], skill:'devops', setupDays:5, userRec:null },
{ id:'sec-cmk', name:'Customer Managed Keys (CMK)', desc:'Krypter data med egne nokler. Full kontroll over kryptering. Viktig for gradert informasjon.', aisle:'security', sources:['foundry','openai','search'], cost:'low', costEst:30, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:[], skill:'devops', setupDays:3, userRec:null },
{ id:'sec-eudb', name:'EU Data Boundary', desc:'Garanti for at data lagres og prosesseres i EU. Dekker Azure, M365, Dynamics 365, Power Platform.', aisle:'security', sources:['m365','foundry','studio','power'], cost:'free', costEst:0, license:['m365-e3','m365-e5','azure-payg','copilot-studio'], residency:['norway','eu'], maturity:'ga', scenarios:['rag-chatbot','autonomous-agent','copilot-extension','customer-service'], skill:'citizen', setupDays:0, userRec:null },
{ id:'sec-norway', name:'Norway East dataresidens', desc:'Data lagres i Oslo/Stavanger. Product Terms-garanti for M365 kjernetjenester.', aisle:'security', sources:['m365','foundry','openai','search'], cost:'free', costEst:0, license:['m365-e3','m365-e5','azure-payg'], residency:['norway'], maturity:'ga', scenarios:['rag-chatbot','autonomous-agent'], skill:'citizen', setupDays:0, userRec:null },
{ id:'sec-adaptive', name:'Adaptive Protection', desc:'Dynamisk risiko-basert beskyttelse. Justerer DLP-regler basert pa brukeradferd. Krever E5.', aisle:'security', sources:['m365'], cost:'free', costEst:0, license:['m365-e5'], residency:['norway','eu'], maturity:'ga', scenarios:[], skill:'pro', setupDays:3, userRec:null },
{ id:'sec-copilot', name:'Security Copilot (12 agenter)', desc:'AI-drevet sikkerhetsassistent med 12 innebygde agenter. Trusseletterforskning, identitetsanalyse, saarbarhet. Inkludert i E5.', aisle:'security', sources:['m365'], cost:'free', costEst:0, license:['m365-e5'], residency:['norway','eu'], maturity:'ga', scenarios:['autonomous-agent','multi-agent'], skill:'pro', setupDays:5, userRec:'Inkludert i M365 E5 fra nov 2025' },
{ id:'sec-prompt-shield-network', name:'AI Prompt Shield (nettverksniva)', desc:'Nettverksbasert prompt injection-beskyttelse integrert med Entra. Filtrerer ondsinnet input for det naar AI-tjenesten.', aisle:'security', sources:['m365','foundry'], cost:'low', costEst:10, license:['m365-e5','azure-payg'], residency:['norway','eu','global'], maturity:'preview', scenarios:['customer-service','autonomous-agent'], skill:'pro', setupDays:3, userRec:'Preview — defense-in-depth lag' },
// === Kanaler & UX ===
{ id:'ch-teams', name:'Microsoft Teams-integrasjon', desc:'AI-assistent eller agent direkte i Teams. Naturlig for interne brukere. Adaptive Cards-stotte.', aisle:'channels', sources:['studio','m365'], cost:'free', costEst:0, license:['m365-e3','m365-e5','copilot-studio'], residency:['norway','eu'], maturity:'ga', scenarios:['rag-chatbot','copilot-extension','customer-service'], skill:'citizen', setupDays:1, userRec:null },
{ id:'ch-web', name:'Web chat widget', desc:'Embed AI-chat pa nettside. Tilpassbar design. For innbygger-/kundeservice.', aisle:'channels', sources:['studio'], cost:'low', costEst:0, license:['copilot-studio'], residency:['norway','eu'], maturity:'ga', scenarios:['customer-service'], skill:'citizen', setupDays:1, userRec:null },
{ id:'ch-whatsapp', name:'WhatsApp Business', desc:'AI-agent pa WhatsApp via Copilot Studio. Populaer kanal for innbyggerkontakt.', aisle:'channels', sources:['studio'], cost:'low', costEst:50, license:['copilot-studio'], residency:['eu'], maturity:'ga', scenarios:['customer-service'], skill:'pro', setupDays:3, userRec:null },
{ id:'ch-adaptive', name:'Adaptive Cards (rik UI)', desc:'Interaktive kort i Teams med knapper, skjemaer, tabeller. Bedre brukeropplevelse enn ren tekst.', aisle:'channels', sources:['studio','m365'], cost:'free', costEst:0, license:['copilot-studio','copilot-license'], residency:['norway','eu'], maturity:'ga', scenarios:['copilot-extension','customer-service'], skill:'pro', setupDays:2, userRec:null },
{ id:'ch-pages', name:'Copilot Pages (samarbeid)', desc:'Delt AI-arbeidsflate der flere brukere kan samarbeide med Copilot. Inkludert i M365 Copilot.', aisle:'channels', sources:['m365'], cost:'free', costEst:0, license:['copilot-license'], residency:['norway','eu'], maturity:'ga', scenarios:['copilot-extension','reporting'], skill:'citizen', setupDays:0, userRec:null },
{ id:'ch-powerapps', name:'Power Apps canvas-app', desc:'Bygg custom AI-grensesnitt med Power Apps. Koble til AI Builder og Azure AI-tjenester.', aisle:'channels', sources:['power'], cost:'low', costEst:40, license:['power-premium'], residency:['norway','eu'], maturity:'ga', scenarios:['document-classification'], skill:'citizen', setupDays:3, userRec:null },
{ id:'ch-custom-web', name:'Custom web-UI (Foundry)', desc:'Full kontroll over frontend. React/Next.js med Azure AI Foundry backend. For avanserte behov.', aisle:'channels', sources:['foundry'], cost:'medium', costEst:100, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:['intelligent-search','multi-agent'], skill:'devops', setupDays:14, userRec:null },
// === Data & Integrasjon ===
{ id:'data-graph', name:'Microsoft Graph API', desc:'Tilgang til M365-data: kalendere, e-post, filer, organisasjon. Hoveddatakilde for M365-basert AI.', aisle:'data', sources:['graph','m365'], cost:'free', costEst:0, license:['m365-e3','m365-e5'], residency:['norway','eu'], maturity:'ga', scenarios:['copilot-extension','intelligent-search','reporting'], skill:'pro', setupDays:2, userRec:null },
{ id:'data-connectors', name:'1000+ Power Platform Connectors', desc:'Ferdige integrasjoner mot SAP, Salesforce, Oracle, SharePoint m.fl. Premium krever lisens.', aisle:'data', sources:['studio','power'], cost:'low', costEst:0, license:['copilot-studio','power-premium'], residency:['norway','eu'], maturity:'ga', scenarios:['autonomous-agent','customer-service'], skill:'citizen', setupDays:1, userRec:null },
{ id:'data-sharepoint', name:'SharePoint Online som datakilde', desc:'Dokumentbibliotek som RAG-kilde. Automatisk indeksering via Graph connector.', aisle:'data', sources:['m365','search','graph'], cost:'free', costEst:0, license:['m365-e3','m365-e5'], residency:['norway','eu'], maturity:'ga', scenarios:['rag-chatbot','intelligent-search','copilot-extension'], skill:'citizen', setupDays:1, userRec:null },
{ id:'data-blob', name:'Azure Blob / ADLS Storage', desc:'Lagring av store datamengder (dokumenter, bilder, video). Basis for RAG-pipelines.', aisle:'data', sources:['foundry','search'], cost:'low', costEst:20, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:['rag-chatbot','document-classification','intelligent-search'], skill:'pro', setupDays:2, userRec:null },
{ id:'data-cosmos', name:'Cosmos DB', desc:'Globalt distribuert NoSQL-database. Vektor-stotte for RAG. Multi-model (document, graph, table).', aisle:'data', sources:['foundry'], cost:'medium', costEst:100, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:[], skill:'devops', setupDays:5, userRec:null },
{ id:'data-sql', name:'Azure SQL / SQL Server', desc:'Relasjonsdatabase for strukturert data. Integrasjon via indexer i Azure AI Search.', aisle:'data', sources:['search','foundry'], cost:'medium', costEst:100, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:[], skill:'pro', setupDays:3, userRec:null },
{ id:'data-dataverse', name:'Dataverse (Power Platform)', desc:'Relasjonsdatabase for Power Platform. Inkludert med Power Apps/Automate Premium.', aisle:'data', sources:['power','studio'], cost:'free', costEst:0, license:['power-premium','copilot-studio'], residency:['norway','eu'], maturity:'ga', scenarios:['reporting'], skill:'citizen', setupDays:1, userRec:null },
{ id:'data-custom-api', name:'Custom API-connectors', desc:'Bygg egne connectors til fagsystemer (NOARK, ePhorte, Public 360). REST eller SOAP.', aisle:'data', sources:['studio','power','foundry'], cost:'low', costEst:0, license:['copilot-studio','power-premium','azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:['autonomous-agent'], skill:'devops', setupDays:10, userRec:null },
{ id:'data-fabric-ai', name:'Fabric AI Functions', desc:'ai.embed() og ai.generate_response() direkte i Microsoft Fabric. AI integrert i data-lakehouse uten separat tjeneste.', aisle:'data', sources:['foundry'], cost:'medium', costEst:100, license:['azure-payg'], residency:['eu','global'], maturity:'ga', scenarios:['reporting','intelligent-search'], skill:'pro', setupDays:5, userRec:null },
// === Observability ===
{ id:'obs-monitor', name:'Azure Monitor / Application Insights', desc:'Full observability for Azure-ressurser. Logging, metriker, alerts, dashboards.', aisle:'observability', sources:['foundry','openai','search'], cost:'low', costEst:30, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:['rag-chatbot','multi-agent','autonomous-agent','intelligent-search'], skill:'pro', setupDays:3, userRec:null },
{ id:'obs-studio-analytics', name:'Copilot Studio Analytics', desc:'Innebygd telemetri for Copilot Studio-agenter. Meldinger/dag, topic-bruk, CSAT.', aisle:'observability', sources:['studio'], cost:'free', costEst:0, license:['copilot-studio'], residency:['norway','eu'], maturity:'ga', scenarios:['customer-service'], skill:'citizen', setupDays:0, userRec:null },
{ id:'obs-cost-mgmt', name:'Azure Cost Management', desc:'Budsjettering, kostnadsanalyse, alerts. Kritisk for a unnga kostnadsoverraskelser.', aisle:'observability', sources:['foundry','openai','search'], cost:'free', costEst:0, license:['azure-payg'], residency:['norway','eu','global'], maturity:'ga', scenarios:['rag-chatbot','multi-agent'], skill:'citizen', setupDays:1, userRec:null },
{ id:'obs-evaluation', name:'AI Foundry Evaluation Tools', desc:'Test groundedness, relevance, coherence av AI-svar. Automatisert kvalitetssikring.', aisle:'observability', sources:['foundry'], cost:'low', costEst:10, license:['azure-payg'], residency:['eu','global'], maturity:'ga', scenarios:['rag-chatbot','multi-agent'], skill:'pro', setupDays:3, userRec:null },
{ id:'obs-compliance', name:'Microsoft Purview Compliance Manager', desc:'Dashboard for compliance-status. GDPR, ISO 27001, AI Act. Automatiserte assessments.', aisle:'observability', sources:['m365'], cost:'free', costEst:0, license:['m365-e3','m365-e5'], residency:['norway','eu'], maturity:'ga', scenarios:['autonomous-agent'], skill:'citizen', setupDays:1, userRec:null },
{ id:'obs-audit', name:'M365 Audit Logging', desc:'Detaljert logg over alle brukerhandlinger i M365. Viktig for arkivlova og revisjon.', aisle:'observability', sources:['m365'], cost:'free', costEst:0, license:['m365-e3','m365-e5'], residency:['norway','eu'], maturity:'ga', scenarios:['copilot-extension'], skill:'citizen', setupDays:0, userRec:null },
{ id:'obs-pp-admin', name:'Power Platform Admin Center', desc:'Copilot Credit-overvaking, AI Builder-forbruk, kapasitetsallokering per miljo.', aisle:'observability', sources:['power','studio'], cost:'free', costEst:0, license:['copilot-studio','power-premium'], residency:['norway','eu'], maturity:'ga', scenarios:[], skill:'citizen', setupDays:0, userRec:null }
];
// =========================================================================
// DATA MODEL — SCENARIOS, COMMAND PIPELINES, CONSTANTS
// =========================================================================
const SCENARIOS = [
{
id: 'rag-chatbot', name: 'RAG-chatbot for interne dokumenter', icon: '\u{1F4DA}',
desc: 'Ansatte sporr en chatbot som henter svar fra SharePoint-dokumenter og fagsystem-data.',
items: ['llm-gpt4o','rag-hybrid','rag-graph-connector','auth-entra','auth-managed-id','sec-sensitivity','sec-eudb','ch-teams','data-sharepoint','obs-monitor'],
reasons: { 'llm-gpt4o':'Balanse mellom kvalitet og kostnad for dokumentforstaaelse', 'rag-hybrid':'Best recall med bade semantisk og keyword-sok', 'rag-graph-connector':'Indekserer SharePoint-innhold automatisk', 'auth-entra':'SSO for alle interne brukere', 'auth-managed-id':'Sikker tilkobling mellom tjenester uten secrets', 'sec-sensitivity':'Respekterer dokumentklassifisering i RAG-svar', 'sec-eudb':'Data forblir i EU — nodvendig for offentlig sektor', 'ch-teams':'Naturlig tilgangsflate for interne brukere', 'data-sharepoint':'Hoveddatakilde for interne dokumenter', 'obs-monitor':'Overvak ytelse og feilrate' }
},
{
id: 'autonomous-agent', name: 'Autonom agent for saksbehandling', icon: '\u{1F3DB}',
desc: 'Agent som behandler innkommende saker, klassifiserer, forbereder vedtak og eskalerer til saksbehandler.',
items: ['llm-gpt4o','rag-hybrid','agent-studio-auto','auth-entra','auth-managed-id','auth-rbac','sec-purview-dlp','sec-eudb','sec-norway','data-connectors','data-custom-api','obs-monitor','obs-compliance'],
reasons: { 'llm-gpt4o':'Nodvendig kvalitet for saksbehandlings-kontekst', 'rag-hybrid':'Sok i regelverk og tidligere vedtak', 'agent-studio-auto':'Kjorer i bakgrunnen, trigget av hendelser', 'auth-entra':'Sikker tilgang for saksbehandlere', 'auth-managed-id':'Tjeneste-til-tjeneste autentisering', 'auth-rbac':'Granulert tilgang basert pa rolle', 'sec-purview-dlp':'Hindrer lekkasje av sensitiv saksinformasjon', 'sec-eudb':'EU Data Boundary-krav', 'sec-norway':'Dataresidens i Norge for personopplysninger', 'data-connectors':'Koble til fagsystemer', 'data-custom-api':'Integrasjon med NOARK/ePhorte', 'obs-monitor':'Overvak agent-ytelse og feilrate', 'obs-compliance':'Dokumenter compliance for revisjon' }
},
{
id: 'document-classification', name: 'Dokumentklassifisering og -prosessering', icon: '\u{1F4C4}',
desc: 'Automatisk klassifisering, OCR og dataekstraksjon fra innkommende dokumenter (PDF, bilder, skjemaer).',
items: ['llm-gpt4o-mini','agent-power-automate','auth-entra','data-blob','ch-powerapps','obs-monitor'],
reasons: { 'llm-gpt4o-mini':'Kostnadseffektiv for hoyvolum-klassifisering', 'agent-power-automate':'Automatiserte flyter for dokumentprosessering', 'auth-entra':'SSO for brukere', 'data-blob':'Lagring av innkommende dokumenter', 'ch-powerapps':'Grensesnitt for manuell gjennomgang', 'obs-monitor':'Spor prosesseringsvolum og feilrate' }
},
{
id: 'multi-agent', name: 'Multi-agent workflow', icon: '\u{1F916}',
desc: 'Flere spesialiserte agenter som samarbeider: forsker, skribent, kvalitetssjekker, godkjenner.',
items: ['llm-gpt4o','llm-router','rag-hybrid','agent-foundry','agent-sk','auth-entra','auth-managed-id','auth-rbac','sec-private','obs-monitor','obs-evaluation','obs-cost-mgmt','ch-custom-web'],
reasons: { 'llm-gpt4o':'Kvalitetsmodell for agentenes resonnering', 'llm-router':'Optimaliser kostnad ved a rute enkle oppgaver til billigere modeller', 'rag-hybrid':'Delt kunnskapsbase for alle agenter', 'agent-foundry':'Full kontroll over agent-logikk og orkestrering', 'agent-sk':'Programmatisk kontroll over planlegging og tool use', 'auth-entra':'Sikker tilgang', 'auth-managed-id':'Tjeneste-til-tjeneste uten secrets', 'auth-rbac':'Begrens hva hver agent kan gjore', 'sec-private':'Nettverksisolering for sensitive data', 'obs-monitor':'Overvak alle agenter sentralt', 'obs-evaluation':'Automatisert kvalitetssikring av output', 'obs-cost-mgmt':'Multi-agent kan drive kostnader — overvak nodvendig', 'ch-custom-web':'Custom dashboard for a folge agent-arbeid' }
},
{
id: 'copilot-extension', name: 'Copilot-utvidelse for M365', icon: '\u{2728}',
desc: 'Utvid M365 Copilot med organisasjonsspesifikke data og handlinger via deklarative agenter.',
items: ['llm-m365','agent-builder','auth-entra','auth-conditional','auth-mfa','sec-purview-dlp','sec-sensitivity','sec-eudb','ch-teams','ch-adaptive','ch-pages','data-graph','data-sharepoint','obs-audit','rag-m365-retrieval'],
reasons: { 'llm-m365':'Managed GPT inkludert i Copilot-lisensen', 'agent-builder':'No-code agenter for M365-kontekst', 'auth-entra':'SSO for alle M365-brukere', 'auth-conditional':'Kontekstbasert tilgang for ekstra sikkerhet', 'auth-mfa':'Obligatorisk for offentlig sektor', 'sec-purview-dlp':'Hindrer datalekkasje via Copilot-svar', 'sec-sensitivity':'Copilot respekterer dokumentklassifisering', 'sec-eudb':'EU Data Boundary for all Copilot-trafikk', 'ch-teams':'Copilot lever i Teams', 'ch-adaptive':'Rike interaktive kort i svar', 'ch-pages':'Samarbeid med Copilot i delte arbeidsflater', 'data-graph':'Tilgang til alle M365-data', 'data-sharepoint':'Dokumenter og filer som kunnskapskilde', 'obs-audit':'Revisjonsspor for Copilot-bruk', 'rag-m365-retrieval':'Programmatisk RAG over M365-data' }
},
{
id: 'customer-service', name: 'Kundeservice-chatbot', icon: '\u{1F4DE}',
desc: 'Ekstern chatbot for innbygger-/kundehenvendelser pa web og WhatsApp med live agent handoff.',
items: ['llm-studio-gen','llm-gpt4o-mini','rag-tenant-grounding','agent-studio-topics','auth-entra','sec-content-safety','sec-prompt-shields','sec-eudb','ch-teams','ch-web','ch-whatsapp','data-connectors','obs-studio-analytics'],
reasons: { 'llm-studio-gen':'Innebygd LLM i Copilot Studio', 'llm-gpt4o-mini':'Kostnadseffektiv for hoyvolum kundehenvendelser', 'rag-tenant-grounding':'Svar basert pa organisasjonens data', 'agent-studio-topics':'Enkel opprettelse av samtaleflyter', 'auth-entra':'SSO for interne agenter som overvaker', 'sec-content-safety':'Filtrer skadelig innhold i svar til kunder', 'sec-prompt-shields':'Beskytt mot misbruk fra eksterne brukere', 'sec-eudb':'EU-residens for kundedata', 'ch-teams':'Intern visning og eskalering', 'ch-web':'Chat-widget pa nettsiden', 'ch-whatsapp':'Populaer kanal for innbyggerkontakt', 'data-connectors':'Koble til CRM og fagsystemer', 'obs-studio-analytics':'Spor tilfredshet og vanlige sporsmal' }
},
{
id: 'intelligent-search', name: 'Intelligent sok pa tvers av fagsystemer', icon: '\u{1F50E}',
desc: 'Samlet sokegrensesnitt som finner informasjon pa tvers av SharePoint, fagsystemer og databaser.',
items: ['llm-gpt4o','rag-hybrid','rag-integrated','rag-graph-connector','auth-entra','auth-managed-id','data-graph','data-sharepoint','data-blob','obs-monitor','ch-custom-web'],
reasons: { 'llm-gpt4o':'Forstaaelse av naturlig-sprak-sporsmal', 'rag-hybrid':'Hybrid sok for best mulig recall', 'rag-integrated':'Automatisk indeksering av nye dokumenter', 'rag-graph-connector':'Indekser M365-data', 'auth-entra':'SSO og tilgangskontroll', 'auth-managed-id':'Sikker tilkobling mellom sokeindeks og datakilder', 'data-graph':'M365 som datakilde', 'data-sharepoint':'Dokumentbibliotek', 'data-blob':'Blob-lagring for store datamengder', 'obs-monitor':'Overvak sokeytelse og relevans', 'ch-custom-web':'Tilpasset sokegrensesnitt' }
},
{
id: 'reporting', name: 'AI-assistert rapportering', icon: '\u{1F4CA}',
desc: 'Automatisk generering av rapporter, sammendrag og analyser fra organisasjonsdata.',
items: ['llm-m365','llm-gpt4o-mini','agent-power-automate','auth-entra','ch-pages','data-graph','data-dataverse','obs-audit'],
reasons: { 'llm-m365':'Copilot i Excel og Word for rapportgenerering', 'llm-gpt4o-mini':'Kostnadseffektiv for bulk-sammendrag', 'agent-power-automate':'Automatiserte rapporteringsflyter', 'auth-entra':'SSO for rapportbrukere', 'ch-pages':'Delt arbeidsflate for rapportsamarbeid', 'data-graph':'Hent data fra hele M365', 'data-dataverse':'Strukturert data for rapportering', 'obs-audit':'Spor hvem som genererer hvilke rapporter' }
}
];
const COMMAND_PIPELINES = {
'rag-chatbot': {
platforms: ['Azure AI Search + OpenAI', 'Copilot Studio'],
commands: [
{ cmd: 'utredning', args: '', desc: 'Komplett utredningsdokument' },
{ cmd: 'compare', args: 'Azure AI Search+OpenAI vs Copilot Studio for RAG-chatbot', desc: 'Plattformsammenligning' },
{ cmd: 'security', args: '', desc: '6-dimensjons sikkerhetsscoring' },
{ cmd: 'cost', args: '', desc: 'Kostnadsestimat i NOK' },
{ cmd: 'diagram', args: 'architecture', desc: 'Arkitekturdiagram' },
{ cmd: 'summary', args: '', desc: 'Teknisk sammendrag' },
{ cmd: 'adr', args: '', desc: 'Architecture Decision Record' }
]
},
'autonomous-agent': {
platforms: ['Copilot Studio', 'Azure AI Foundry'],
commands: [
{ cmd: 'utredning', args: '', desc: 'Komplett utredningsdokument' },
{ cmd: 'compare', args: 'Copilot Studio Autonomous Agents vs Foundry Agent Service', desc: 'Plattformsammenligning' },
{ cmd: 'security', args: '', desc: '6-dimensjons sikkerhetsscoring' },
{ cmd: 'dpia', args: '', desc: 'Personvernkonsekvensvurdering' },
{ cmd: 'cost', args: '', desc: 'Kostnadsestimat i NOK' },
{ cmd: 'diagram', args: 'architecture', desc: 'Arkitekturdiagram' },
{ cmd: 'summary', args: '', desc: 'Teknisk sammendrag' },
{ cmd: 'adr', args: '', desc: 'Architecture Decision Record' }
]
},
'document-classification': {
platforms: ['Power Platform AI', 'Azure AI Services'],
commands: [
{ cmd: 'utredning', args: '', desc: 'Komplett utredningsdokument' },
{ cmd: 'compare', args: 'Power Automate AI vs Azure AI Document Intelligence', desc: 'Plattformsammenligning' },
{ cmd: 'cost', args: '', desc: 'Kostnadsestimat i NOK' },
{ cmd: 'summary', args: '', desc: 'Teknisk sammendrag' }
]
},
'multi-agent': {
platforms: ['Azure AI Foundry + Semantic Kernel', 'Copilot Studio MCP'],
commands: [
{ cmd: 'utredning', args: '', desc: 'Komplett utredningsdokument' },
{ cmd: 'compare', args: 'Foundry+SK vs Copilot Studio MCP for multi-agent', desc: 'Plattformsammenligning' },
{ cmd: 'security', args: '', desc: '6-dimensjons sikkerhetsscoring' },
{ cmd: 'cost', args: '', desc: 'Kostnadsestimat i NOK' },
{ cmd: 'diagram', args: 'dataflow', desc: 'Dataflytdiagram' },
{ cmd: 'summary', args: '', desc: 'Teknisk sammendrag' },
{ cmd: 'adr', args: '', desc: 'Architecture Decision Record' }
]
},
'copilot-extension': {
platforms: ['M365 Copilot + Agent Builder'],
commands: [
{ cmd: 'utredning', args: '', desc: 'Komplett utredningsdokument' },
{ cmd: 'license', args: '', desc: 'Lisenskartlegging' },
{ cmd: 'security', args: '', desc: '6-dimensjons sikkerhetsscoring' },
{ cmd: 'cost', args: '', desc: 'Kostnadsestimat i NOK' },
{ cmd: 'summary', args: '', desc: 'Teknisk sammendrag' }
]
},
'customer-service': {
platforms: ['Copilot Studio'],
commands: [
{ cmd: 'utredning', args: '', desc: 'Komplett utredningsdokument' },
{ cmd: 'security', args: '', desc: '6-dimensjons sikkerhetsscoring (ekstern eksponering)' },
{ cmd: 'dpia', args: '', desc: 'Personvernkonsekvensvurdering' },
{ cmd: 'cost', args: '', desc: 'Kostnadsestimat i NOK' },
{ cmd: 'diagram', args: 'architecture', desc: 'Arkitekturdiagram' },
{ cmd: 'summary', args: '', desc: 'Teknisk sammendrag' },
{ cmd: 'adr', args: '', desc: 'Architecture Decision Record' }
]
},
'intelligent-search': {
platforms: ['Azure AI Search', 'M365 Search'],
commands: [
{ cmd: 'utredning', args: '', desc: 'Komplett utredningsdokument' },
{ cmd: 'compare', args: 'Azure AI Search vs M365 Search for enterprise-sok', desc: 'Plattformsammenligning' },
{ cmd: 'cost', args: '', desc: 'Kostnadsestimat i NOK' },
{ cmd: 'diagram', args: 'dataflow', desc: 'Dataflytdiagram' },
{ cmd: 'summary', args: '', desc: 'Teknisk sammendrag' }
]
},
'reporting': {
platforms: ['M365 Copilot + Power Platform'],
commands: [
{ cmd: 'utredning', args: '', desc: 'Komplett utredningsdokument' },
{ cmd: 'license', args: '', desc: 'Lisenskartlegging' },
{ cmd: 'cost', args: '', desc: 'Kostnadsestimat i NOK' },
{ cmd: 'summary', args: '', desc: 'Teknisk sammendrag' }
]
},
'custom': {
platforms: [],
commands: [
{ cmd: 'utredning', args: '', desc: 'Komplett utredningsdokument' },
{ cmd: 'security', args: '', desc: '6-dimensjons sikkerhetsscoring' },
{ cmd: 'cost', args: '', desc: 'Kostnadsestimat i NOK' },
{ cmd: 'summary', args: '', desc: 'Teknisk sammendrag' },
{ cmd: 'adr', args: '', desc: 'Architecture Decision Record' }
]
}
};
const LICENSE_NAMES = {
'm365-e3': 'Microsoft 365 E3',
'm365-e5': 'Microsoft 365 E5',
'copilot-license': 'M365 Copilot add-on',
'copilot-studio': 'Copilot Studio',
'azure-payg': 'Azure Pay-as-you-go',
'power-premium': 'Power Platform Premium'
};
const ORG_TYPE_NAMES = {
'statlig-etat': 'Statlig etat',
'kommune': 'Kommune',
'fylkeskommune': 'Fylkeskommune',
'universitet': 'Universitet/hogskole',
'helsevesen': 'Helsevesen',
'privat': 'Privat virksomhet'
};
const NOK_RATE = 11;
// =========================================================================
// STATE
// =========================================================================
const state = {
currentStep: 0, // 0=hero, 1-5=steps
wizardSlide: 1,
intake: {
orgType: '',
size: '',
licenses: [],
compliance: [],
residency: 'eu',
scenario: '',
freetext: '',
users: '',
volume: '',
budget: '',
timeframe: '',
target: '',
aiActLevel: '', // '', 'minimal', 'limited', 'high-risk', 'prohibited'
aiActRole: '' // '', 'deployer', 'provider', 'deployer-provider'
},
cart: [], // [{id, reason}]
activeScenario: null,
searchQuery: ''
};
// =========================================================================
// INIT
// =========================================================================
function init() {
document.getElementById('totalItems').textContent = ITEMS.length;
renderWizardScenarios();
}
// =========================================================================
// ENTRY MODES
// =========================================================================
function startGuided() {
document.getElementById('heroSection').classList.add('hidden');
document.getElementById('stepper').classList.remove('hidden');
goToStep(1);
}
function startExplore() {
document.getElementById('heroSection').classList.add('hidden');
document.getElementById('stepper').classList.remove('hidden');
goToStep(2);
renderAisles();
}
function startExpert() {
document.getElementById('heroSection').classList.add('hidden');
document.getElementById('stepper').classList.remove('hidden');
goToStep(2);
renderAisles();
}
function startOver() {
// Reset state
state.currentStep = 0;
state.wizardSlide = 1;
state.intake = { orgType:'', size:'', licenses:[], compliance:[], residency:'eu', scenario:'', freetext:'', users:'', volume:'', budget:'', timeframe:'', target:'' };
state.cart = [];
state.activeScenario = null;
state.searchQuery = '';
// Reset UI
document.getElementById('heroSection').classList.remove('hidden');
document.getElementById('stepper').classList.add('hidden');
for (let i = 1; i <= 5; i++) {
document.getElementById('step-' + i).classList.add('hidden');
}
// Reset wizard slides
document.querySelectorAll('.wizard-slide').forEach((s, i) => {
s.classList.toggle('active', i === 0);
});
document.querySelectorAll('.wizard-card').forEach(c => c.classList.remove('selected'));
document.querySelectorAll('.wizard-check input').forEach(cb => cb.checked = false);
document.querySelectorAll('.wizard-check').forEach(c => c.classList.remove('selected'));
updateStepper();
updateCartBadge();
}
// =========================================================================
// STEPPER NAVIGATION
// =========================================================================
function goToStep(n) {
// Only allow navigation to completed steps or next step
if (n > state.currentStep + 1 && n > 2) return;
state.currentStep = n;
for (let i = 1; i <= 5; i++) {
document.getElementById('step-' + i).classList.toggle('hidden', i !== n);
}
updateStepper();
// Trigger rendering for specific steps
if (n === 2) renderAisles();
if (n === 3) renderConfigure();
if (n === 4) renderReview();
if (n === 5) renderExport();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function updateStepper() {
document.querySelectorAll('.step').forEach(el => {
const stepNum = parseInt(el.dataset.step);
el.classList.remove('active', 'completed');
if (stepNum === state.currentStep) el.classList.add('active');
else if (stepNum < state.currentStep) el.classList.add('completed');
});
}
// =========================================================================
// WIZARD — SLIDE NAVIGATION
// =========================================================================
function nextSlide() {
const slides = document.querySelectorAll('.wizard-slide');
if (state.wizardSlide >= slides.length) return;
slides[state.wizardSlide - 1].classList.remove('active');
state.wizardSlide++;
slides[state.wizardSlide - 1].classList.add('active');
}
function prevSlide() {
if (state.wizardSlide <= 1) return;
const slides = document.querySelectorAll('.wizard-slide');
slides[state.wizardSlide - 1].classList.remove('active');
state.wizardSlide--;
slides[state.wizardSlide - 1].classList.add('active');
}
// =========================================================================
// WIZARD — SELECTION HANDLERS
// =========================================================================
function setOrgType(el, value) {
state.intake.orgType = value;
el.closest('.wizard-grid').querySelectorAll('.wizard-card').forEach(c => c.classList.remove('selected'));
el.classList.add('selected');
}
function setSize(el, value) {
state.intake.size = value;
el.closest('.wizard-grid').querySelectorAll('.wizard-card').forEach(c => c.classList.remove('selected'));
el.classList.add('selected');
}
function toggleCheck(el) {
// The label click toggles the checkbox; update .selected class
setTimeout(() => {
const cb = el.querySelector('input[type="checkbox"]');
el.classList.toggle('selected', cb.checked);
}, 0);
}
function updateIntakeLicenses() {
state.intake.licenses = [];
document.querySelectorAll('[data-slide="3"] input[type="checkbox"]:checked').forEach(cb => {
state.intake.licenses.push(cb.value);
});
}
function updateIntakeCompliance() {
state.intake.compliance = [];
document.querySelectorAll('[data-slide="4"] .wizard-checks input[type="checkbox"]:checked').forEach(cb => {
state.intake.compliance.push(cb.value);
});
}
function updateAiActLevel(level) {
state.intake.aiActLevel = level;
// Sync legacy compliance array
const idx = state.intake.compliance.indexOf('ai-act-high');
if (level === 'high-risk' || level === 'prohibited') {
if (idx === -1) state.intake.compliance.push('ai-act-high');
} else {
if (idx !== -1) state.intake.compliance.splice(idx, 1);
}
}
function updateIntakeResidency() {
const checked = document.querySelector('input[name="residency"]:checked');
if (checked) state.intake.residency = checked.value;
}
function setScenario(el, scenarioId) {
if (state.intake.scenario === scenarioId) {
state.intake.scenario = '';
el.classList.remove('active');
} else {
state.intake.scenario = scenarioId;
el.closest('.scenarios-grid').querySelectorAll('.scenario-card').forEach(c => c.classList.remove('active'));
el.classList.add('active');
}
}
function renderWizardScenarios() {
const grid = document.getElementById('wizardScenarios');
if (!grid) return;
grid.innerHTML = SCENARIOS.map(s => `
<div class="scenario-card ${state.intake.scenario === s.id ? 'active' : ''}" onclick="setScenario(this,'${s.id}')">
<div class="scenario-icon">${s.icon}</div>
<h3>${s.name}</h3>
<p>${s.desc}</p>
<div class="scenario-items-count">${s.items.length} kapabiliteter inkludert</div>
</div>
`).join('');
}
// =========================================================================
// WIZARD — SUBMIT
// =========================================================================
function submitWizard() {
// Apply scenario to cart if selected
if (state.intake.scenario) {
const scenario = SCENARIOS.find(s => s.id === state.intake.scenario);
if (scenario) {
state.activeScenario = scenario.id;
state.cart = scenario.items.map(itemId => ({
id: itemId,
reason: scenario.reasons[itemId] || ''
}));
}
}
// Apply intake licenses to sidebar filters
if (state.intake.licenses.length > 0) {
applyIntakeToFilters();
}
// Move to step 2
goToStep(2);
}
function applyIntakeToFilters() {
// Set sidebar license checkboxes based on intake
document.querySelectorAll('input[data-filter="license"]').forEach(cb => {
cb.checked = state.intake.licenses.length === 0 || state.intake.licenses.includes(cb.value);
});
// Set residency based on intake
document.querySelectorAll('input[data-filter="residency"]').forEach(cb => {
if (state.intake.residency === 'norway') {
cb.checked = cb.value === 'norway';
} else if (state.intake.residency === 'eu') {
cb.checked = cb.value === 'norway' || cb.value === 'eu';
} else {
cb.checked = true;
}
});
// Apply compliance filters
if (state.intake.compliance.includes('schrems-ii')) {
const schremsBox = document.querySelector('input[data-filter="compliance"][value="schrems-ii"]');
if (schremsBox) schremsBox.checked = true;
}
}
// =========================================================================
// RENDERING — AISLES & ITEMS
// =========================================================================
function renderAisles() {
const container = document.getElementById('aislesContainer');
if (!container) return;
container.innerHTML = AISLES.map(aisle => {
const aisleItems = ITEMS.filter(i => i.aisle === aisle.id);
return `
<div class="aisle" data-aisle="${aisle.id}">
<div class="aisle-header">
<span class="aisle-icon">${aisle.icon}</span>
<h3>${aisle.name}</h3>
<span class="aisle-count">${aisle.desc}</span>
</div>
<div class="items-grid">
${aisleItems.map(item => renderItem(item)).join('')}
</div>
</div>
`;
}).join('');
applyFilters();
applyIntakeConstraints();
}
function renderItem(item) {
const isSelected = state.cart.some(c => c.id === item.id);
const cartEntry = state.cart.find(c => c.id === item.id);
const reason = cartEntry?.reason || '';
const skillLabel = item.skill === 'citizen' ? 'No-code' : item.skill === 'pro' ? 'Low-code' : 'Code-first';
const skillClass = 'skill-' + item.skill;
return `
<div class="item-card ${isSelected ? 'selected' : ''} ${reason ? 'has-reason' : ''}"
data-id="${item.id}" onclick="toggleItem('${item.id}')">
<div class="item-name">${item.name}</div>
<div class="item-desc">${item.desc}</div>
<div class="item-meta">
${item.sources.map(s => `<span class="item-source ${BRANDS[s].color}">${BRANDS[s].short}</span>`).join('')}
<span class="item-badge badge-${item.maturity}">${item.maturity.toUpperCase()}</span>
<span class="item-badge badge-${item.cost}">${item.cost === 'free' ? 'Gratis' : item.cost === 'low' ? 'Lav' : item.cost === 'medium' ? 'Medium' : 'Hoy'}</span>
<span class="skill-badge ${skillClass}">${skillLabel}</span>
</div>
${reason ? `<div class="item-why">${reason}</div>` : ''}
</div>
`;
}
// =========================================================================
// INTAKE CONSTRAINT APPLICATION
// =========================================================================
function applyIntakeConstraints() {
if (state.intake.licenses.length === 0 && state.intake.residency === 'global') return;
document.querySelectorAll('.item-card').forEach(card => {
const id = card.dataset.id;
const item = ITEMS.find(i => i.id === id);
if (!item) return;
let mismatch = false;
// License mismatch: item requires licenses user doesn't have
if (state.intake.licenses.length > 0) {
const hasMatchingLicense = item.license.some(l => state.intake.licenses.includes(l));
if (!hasMatchingLicense) mismatch = true;
}
// Residency mismatch: item doesn't support required residency
if (state.intake.residency === 'norway') {
if (!item.residency.includes('norway')) mismatch = true;
} else if (state.intake.residency === 'eu') {
if (!item.residency.includes('norway') && !item.residency.includes('eu')) mismatch = true;
}
card.classList.toggle('intake-mismatch', mismatch);
});
}
// =========================================================================
// SEARCH & FILTERS
// =========================================================================
function searchItems(query) {
state.searchQuery = query.toLowerCase();
document.querySelectorAll('.item-card').forEach(card => {
const id = card.dataset.id;
const item = ITEMS.find(i => i.id === id);
if (!item) return;
const text = (item.name + ' ' + item.desc + ' ' + item.sources.map(s => BRANDS[s].name).join(' ')).toLowerCase();
card.style.display = text.includes(state.searchQuery) || !state.searchQuery ? '' : 'none';
});
}
function applyFilters() {
const licenses = getCheckedValues('license');
const residencies = getCheckedValues('residency');
const complianceFilters = getCheckedValues('compliance');
const maturities = getCheckedValues('maturity');
const costs = getCheckedValues('cost');
document.querySelectorAll('.item-card').forEach(card => {
const id = card.dataset.id;
const item = ITEMS.find(i => i.id === id);
if (!item) return;
let visible = true;
if (licenses.length > 0) {
if (!item.license.some(l => licenses.includes(l))) visible = false;
}
if (residencies.length > 0) {
if (!item.residency.some(r => residencies.includes(r))) visible = false;
}
if (maturities.length > 0 && !maturities.includes(item.maturity)) visible = false;
if (costs.length > 0 && !costs.includes(item.cost)) visible = false;
if (complianceFilters.includes('schrems-ii')) {
if (!item.residency.includes('norway') && !item.residency.includes('eu')) visible = false;
}
card.classList.toggle('filtered-out', !visible);
});
}
function resetFilters() {
document.querySelectorAll('.sidebar input[type="checkbox"]').forEach(cb => {
const filter = cb.dataset.filter;
cb.checked = filter !== 'compliance';
});
applyFilters();
}
function getCheckedValues(filterName) {
const checked = [];
document.querySelectorAll(`input[data-filter="${filterName}"]:checked`).forEach(cb => {
checked.push(cb.value);
});
return checked;
}
// =========================================================================
// CART MANAGEMENT
// =========================================================================
function toggleItem(itemId) {
const idx = state.cart.findIndex(c => c.id === itemId);
if (idx >= 0) {
state.cart.splice(idx, 1);
} else {
state.cart.push({ id: itemId, reason: '' });
state.activeScenario = null;
}
renderAisles();
updateCartBadge();
}
function removeFromCart(itemId) {
state.cart = state.cart.filter(c => c.id !== itemId);
state.activeScenario = null;
updateCartBadge();
// Re-render current step
if (state.currentStep === 2) renderAisles();
if (state.currentStep === 3) renderConfigure();
}
function toggleCart() {
document.getElementById('cartPanel').classList.toggle('open');
if (document.getElementById('cartPanel').classList.contains('open')) {
renderCartPanel();
}
}
function updateCartBadge() {
document.getElementById('cartCount').textContent = state.cart.length;
}
function renderCartPanel() {
const container = document.getElementById('cartItems');
if (state.cart.length === 0) {
container.innerHTML = '<div class="cart-empty">Klikk pa kapabiliteter for a legge dem i handlekurven.</div>';
return;
}
const grouped = groupCartByAisle();
let html = '';
Object.keys(grouped).forEach(aisleName => {
html += `<div style="font-size:0.7rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.05em;padding:0.5rem 0 0.25rem;margin-top:0.25rem">${aisleName}</div>`;
grouped[aisleName].forEach(item => {
html += `
<div class="cart-item">
<div class="cart-item-row">
<span class="cart-item-name">${item.name}</span>
<button class="cart-item-remove" onclick="event.stopPropagation();removeFromCart('${item.id}')">&times;</button>
</div>
${item.reason ? `<div style="font-size:0.68rem;color:var(--accent4);font-style:italic">${item.reason}</div>` : ''}
</div>
`;
});
});
container.innerHTML = html;
}
// =========================================================================
// HELPERS
// =========================================================================
function groupCartByAisle() {
const grouped = {};
state.cart.forEach(entry => {
const item = ITEMS.find(i => i.id === entry.id);
if (!item) return;
const aisle = AISLES.find(a => a.id === item.aisle);
const aisleName = aisle ? aisle.name : 'Annet';
if (!grouped[aisleName]) grouped[aisleName] = [];
grouped[aisleName].push({ ...item, reason: entry.reason });
});
return grouped;
}
function getCartTotalCost() {
return state.cart.reduce((sum, entry) => {
const item = ITEMS.find(i => i.id === entry.id);
return sum + (item ? item.costEst : 0);
}, 0);
}
function getCartLicenses() {
const licenses = new Set();
state.cart.forEach(entry => {
const item = ITEMS.find(i => i.id === entry.id);
if (item) item.license.forEach(l => licenses.add(l));
});
return [...licenses];
}
function getUserMultiplier() {
const users = state.intake.users || '';
if (users === '5000+') return 3;
if (users === '1000-5000') return 2;
if (users === '200-1000') return 1.5;
if (users === '50-200') return 1.2;
return 1;
}
// =========================================================================
// STEP 3: CONFIGURE
// =========================================================================
function renderConfigure() {
const container = document.getElementById('configureContent');
if (state.cart.length === 0) {
container.innerHTML = '<div class="cart-empty" style="padding:3rem;text-align:center">Ingen kapabiliteter valgt. Ga tilbake til Utforsk og legg til i handlekurven.</div>';
return;
}
const grouped = groupCartByAisle();
const totalCost = getCartTotalCost();
let cartHtml = '';
Object.keys(grouped).forEach(aisleName => {
cartHtml += `<div style="font-size:0.75rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.05em;padding:0.75rem 0 0.3rem">${aisleName}</div>`;
grouped[aisleName].forEach(item => {
cartHtml += `
<div class="config-item">
<span class="config-item-name">${item.name}</span>
<span class="config-item-cost">${item.costEst > 0 ? '~$' + item.costEst + '/mnd' : 'Inkludert'}</span>
<button class="config-item-remove" onclick="removeFromCart('${item.id}')">&times;</button>
</div>
`;
});
});
const compliance = getComplianceStatus();
let complianceHtml = compliance.map(c => `
<div class="compliance-item">
<span class="traffic-light tl-${c.status}"></span>
<span>${c.label}</span>
</div>
`).join('');
container.innerHTML = `
<div class="config-layout">
<div>
<h3 style="margin-bottom:1rem">${state.cart.length} kapabiliteter valgt</h3>
<div class="config-cart-list">${cartHtml}</div>
</div>
<div class="config-sidebar">
<div class="param-group">
<h4>Parametere</h4>
<div class="param-input">
<label>Antall brukere</label>
<select onchange="state.intake.users=this.value;renderConfigure()">
<option value="" ${!state.intake.users?'selected':''}>Velg...</option>
<option value="1-50" ${state.intake.users==='1-50'?'selected':''}>1-50</option>
<option value="50-200" ${state.intake.users==='50-200'?'selected':''}>50-200</option>
<option value="200-1000" ${state.intake.users==='200-1000'?'selected':''}>200-1000</option>
<option value="1000-5000" ${state.intake.users==='1000-5000'?'selected':''}>1000-5000</option>
<option value="5000+" ${state.intake.users==='5000+'?'selected':''}>5000+</option>
</select>
</div>
<div class="param-input">
<label>Estimert volum/dag</label>
<input type="text" value="${state.intake.volume||''}" placeholder="F.eks.: 500 meldinger" oninput="state.intake.volume=this.value">
</div>
<div class="param-input">
<label>Budsjett (NOK/mnd)</label>
<input type="number" value="${state.intake.budget||''}" placeholder="F.eks.: 50000" oninput="state.intake.budget=this.value">
</div>
</div>
<div class="param-group">
<h4>Estimert kostnad</h4>
<div class="review-stat" style="color:var(--accent4)">${Math.round(totalCost * NOK_RATE).toLocaleString()} NOK/mnd</div>
<div style="font-size:0.78rem;color:var(--text-dim)">~$${totalCost.toLocaleString()} USD (${state.cart.length} tjenester)</div>
</div>
<div class="param-group">
<h4>Compliance-sjekk</h4>
<div class="compliance-list">${complianceHtml}</div>
</div>
${getAiActPanel()}
</div>
</div>
`;
}
function getAiActPanel() {
if (!state.intake.aiActLevel) return '';
const level = state.intake.aiActLevel;
const role = state.intake.aiActRole || 'deployer';
const cmds = [];
if (level === 'high-risk') {
cmds.push({ cmd: '/architect:classify', desc: 'Bekreft klassifisering' });
cmds.push({ cmd: '/architect:frimpact', desc: 'FRIA (obligatorisk offentlig sektor)' });
cmds.push({ cmd: '/architect:dpia', desc: 'Personvernkonsekvensvurdering' });
cmds.push({ cmd: '/architect:requirements', desc: 'Konkrete Art. 9-27 krav' });
} else if (level === 'limited') {
cmds.push({ cmd: '/architect:classify', desc: 'Bekreft klassifisering' });
cmds.push({ cmd: '/architect:transparency', desc: 'Generer Art. 50 transparensnotis' });
} else if (level === 'minimal') {
cmds.push({ cmd: '/architect:classify', desc: 'Dokumenter klassifisering' });
} else if (level === 'prohibited') {
cmds.push({ cmd: '/architect:classify', desc: 'Dokumenter forbudt-klassifisering' });
}
const borderColor = (level === 'high-risk' || level === 'prohibited') ? 'var(--accent7)' : level === 'limited' ? 'var(--accent4)' : 'var(--accent8)';
return `
<div class="param-group" style="border-left:3px solid ${borderColor};padding-left:0.75rem">
<h4>EU AI Act — Neste steg</h4>
${cmds.map(c => `<div style="font-size:0.82rem;margin:0.3rem 0"><code>${c.cmd}</code> — ${c.desc}</div>`).join('')}
${level === 'high-risk' ? '<div style="font-size:0.78rem;color:var(--text-dim);margin-top:0.5rem">Frist: 2. august 2026 (GPAI + Annex III)</div>' : ''}
</div>`;
}
function getComplianceStatus() {
const checks = [];
const hasNorway = state.cart.some(c => { const i = ITEMS.find(x => x.id === c.id); return i && i.residency.includes('norway'); });
const allEU = state.cart.every(c => { const i = ITEMS.find(x => x.id === c.id); return i && (i.residency.includes('norway') || i.residency.includes('eu')); });
const hasGlobalOnly = state.cart.some(c => { const i = ITEMS.find(x => x.id === c.id); return i && !i.residency.includes('norway') && !i.residency.includes('eu'); });
const hasDLP = state.cart.some(c => c.id === 'sec-purview-dlp');
const hasContentSafety = state.cart.some(c => c.id === 'sec-content-safety');
const hasPromptShields = state.cart.some(c => c.id === 'sec-prompt-shields');
const hasIdentity = state.cart.some(c => { const i = ITEMS.find(x => x.id === c.id); return i && i.aisle === 'identity'; });
const hasObservability = state.cart.some(c => { const i = ITEMS.find(x => x.id === c.id); return i && i.aisle === 'observability'; });
const hasExternalChannel = state.cart.some(c => c.id === 'ch-web' || c.id === 'ch-whatsapp');
// Schrems II / Data residency
if (state.intake.compliance.includes('schrems-ii') || state.intake.residency === 'norway') {
checks.push({ label: 'Schrems II / Dataresidens', status: hasGlobalOnly ? 'red' : allEU ? 'green' : 'yellow' });
} else {
checks.push({ label: 'EU Data Boundary', status: allEU ? 'green' : hasGlobalOnly ? 'yellow' : 'green' });
}
// Identity
checks.push({ label: 'Identitet & tilgangsstyring', status: hasIdentity ? 'green' : 'red' });
// DLP
checks.push({ label: 'Data Loss Prevention', status: hasDLP ? 'green' : 'yellow' });
// Content safety for external channels
if (hasExternalChannel) {
checks.push({ label: 'Content Safety (ekstern kanal)', status: hasContentSafety && hasPromptShields ? 'green' : hasContentSafety || hasPromptShields ? 'yellow' : 'red' });
}
// Observability
checks.push({ label: 'Observability & logging', status: hasObservability ? 'green' : 'yellow' });
// DPIA
if (state.intake.compliance.includes('dpia-required')) {
checks.push({ label: 'DPIA/PVK pakrevd', status: 'yellow' });
}
// AI Act
const aiLevel = state.intake.aiActLevel;
if (aiLevel === 'high-risk') {
checks.push({ label: 'AI Act: Hoyrisiko (Annex III)', status: 'red' });
checks.push({ label: 'FRIA pakrevd (Art. 27)', status: 'red' });
if (!hasObservability) {
checks.push({ label: 'Logging min. 6 mnd pakrevd (Art. 12/26)', status: 'red' });
}
} else if (aiLevel === 'limited') {
checks.push({ label: 'AI Act: Begrenset risiko', status: 'yellow' });
checks.push({ label: 'Transparensplikt (Art. 50)', status: 'yellow' });
} else if (aiLevel === 'minimal') {
checks.push({ label: 'AI Act: Minimal risiko', status: 'green' });
} else if (aiLevel === 'prohibited') {
checks.push({ label: 'AI Act: FORBUDT — kan ikke brukes', status: 'red' });
} else if (state.intake.compliance.includes('ai-act-high')) {
// Legacy fallback
checks.push({ label: 'AI Act hoy-risiko', status: 'yellow' });
}
return checks;
}
// =========================================================================
// STEP 4: REVIEW
// =========================================================================
function renderReview() {
const container = document.getElementById('reviewContent');
if (state.cart.length === 0) {
container.innerHTML = '<div class="cart-empty" style="padding:3rem;text-align:center">Ingen kapabiliteter valgt.</div>';
return;
}
const totalCost = getCartTotalCost();
const userMult = getUserMultiplier();
const p10 = Math.round(totalCost * 0.7 * NOK_RATE);
const p50 = Math.round(totalCost * userMult * NOK_RATE);
const p90 = Math.round(totalCost * 1.5 * userMult * NOK_RATE);
const maxCost = p90 || 1;
const licenses = getCartLicenses();
const compliance = getComplianceStatus();
const risks = getRisks();
const recommendations = getRecommendations();
const scenario = state.activeScenario ? SCENARIOS.find(s => s.id === state.activeScenario) : null;
const orgName = state.intake.orgType ? (ORG_TYPE_NAMES[state.intake.orgType] || state.intake.orgType) : 'Ikke spesifisert';
// Card 1: Overview
let overviewHtml = `
<div class="review-card">
<h4>Arkitekturoversikt</h4>
<div class="review-stat">${state.cart.length}</div>
<div class="review-stat-label">kapabiliteter valgt</div>
<ul class="review-list" style="margin-top:1rem">
<li><strong>Scenario:</strong> ${scenario ? scenario.name : 'Egendefinert'}</li>
<li><strong>Organisasjon:</strong> ${orgName}</li>
<li><strong>Brukere:</strong> ${state.intake.users || 'Ikke spesifisert'}</li>
<li><strong>Lisenser:</strong> ${licenses.map(l => LICENSE_NAMES[l] || l).join(', ') || 'Ingen'}</li>
<li><strong>Dataresidens:</strong> ${state.intake.residency === 'norway' ? 'Norway East' : state.intake.residency === 'eu' ? 'EU Data Boundary' : 'Global'}</li>
</ul>
</div>
`;
// Card 2: Cost
let costHtml = `
<div class="review-card">
<h4>Kostnadsestimat</h4>
<div class="review-stat" style="color:var(--accent4)">${p50.toLocaleString()} NOK/mnd</div>
<div class="review-stat-label">P50-estimat (${state.intake.users || 'ukjent antall'} brukere)</div>
<div style="margin-top:1rem">
<div class="cost-range-bar">
<span class="label">P10</span>
<div class="bar"><div class="bar-fill" style="width:${Math.round(p10/maxCost*100)}%;background:var(--accent8)"></div></div>
<span class="value">${p10.toLocaleString()} NOK</span>
</div>
<div class="cost-range-bar">
<span class="label">P50</span>
<div class="bar"><div class="bar-fill" style="width:${Math.round(p50/maxCost*100)}%;background:var(--accent4)"></div></div>
<span class="value">${p50.toLocaleString()} NOK</span>
</div>
<div class="cost-range-bar">
<span class="label">P90</span>
<div class="bar"><div class="bar-fill" style="width:100%;background:var(--accent7)"></div></div>
<span class="value">${p90.toLocaleString()} NOK</span>
</div>
</div>
<div style="margin-top:0.75rem;font-size:0.75rem;color:var(--text-dim)">Multiplikator: x${userMult} (basert pa brukerantall)</div>
</div>
`;
// Card 3: Compliance
let complianceHtml = `
<div class="review-card">
<h4>Compliance</h4>
<ul class="review-list">
${compliance.map(c => `<li><span class="traffic-light tl-${c.status}"></span>${c.label}</li>`).join('')}
</ul>
</div>
`;
// Card 4: Risks & Recommendations
let riskHtml = `
<div class="review-card">
<h4>Risiko og anbefalinger</h4>
${risks.length > 0 ? `<div style="margin-bottom:1rem"><strong style="color:var(--accent7)">Risiko:</strong><ul class="review-list">${risks.map(r => `<li style="color:var(--accent4)">${r}</li>`).join('')}</ul></div>` : '<div style="margin-bottom:1rem;color:var(--accent8)">Ingen kritiske risikoer identifisert.</div>'}
${recommendations.length > 0 ? `<div><strong style="color:var(--accent5)">Anbefalinger:</strong><ul class="review-list">${recommendations.map(r => `<li>${r}</li>`).join('')}</ul></div>` : ''}
</div>
`;
container.innerHTML = overviewHtml + costHtml + complianceHtml + riskHtml;
}
function getRisks() {
const risks = [];
const hasPreview = state.cart.some(c => { const i = ITEMS.find(x => x.id === c.id); return i && i.maturity === 'preview'; });
const totalCost = getCartTotalCost();
const hasObservability = state.cart.some(c => { const i = ITEMS.find(x => x.id === c.id); return i && i.aisle === 'observability'; });
const budget = parseInt(state.intake.budget) || 0;
const p50nok = Math.round(totalCost * getUserMultiplier() * NOK_RATE);
if (hasPreview) risks.push('Preview-tjenester i produksjon — risiko for breaking changes');
if (totalCost > 1000) risks.push('Hoy estimert kostnad — vurder kostnadsoptimering');
if (budget > 0 && p50nok > budget) risks.push(`P50-estimat (${p50nok.toLocaleString()} NOK) overstiger budsjett (${budget.toLocaleString()} NOK)`);
if (!hasObservability) risks.push('Mangler observability — vanskelig a feilsoke og overvake');
return risks;
}
function getRecommendations() {
const recs = [];
const hasRAG = state.cart.some(c => c.id.startsWith('rag-'));
const hasSearch = state.cart.some(c => c.id === 'rag-hybrid' || c.id === 'rag-integrated');
const hasAgent = state.cart.some(c => { const i = ITEMS.find(x => x.id === c.id); return i && i.aisle === 'agent'; });
const hasIdentity = state.cart.some(c => { const i = ITEMS.find(x => x.id === c.id); return i && i.aisle === 'identity'; });
const hasExternalChannel = state.cart.some(c => c.id === 'ch-web' || c.id === 'ch-whatsapp');
const hasContentSafety = state.cart.some(c => c.id === 'sec-content-safety');
if (hasRAG && !hasSearch) recs.push('Vurder Azure AI Search for bedre RAG-ytelse');
if (hasAgent && !hasIdentity) recs.push('Legg til identitetslosning (Entra ID, Managed Identity) for agent-sikkerhet');
if (hasExternalChannel && !hasContentSafety) recs.push('Legg til Content Safety for ekstern-eksponerte kanaler');
if (state.cart.length > 15) recs.push('Mange komponenter — vurder a fase inn i trinn (POC -> Pilot -> Produksjon)');
return recs;
}
// =========================================================================
// STEP 5: EXPORT
// =========================================================================
function renderExport() {
const container = document.getElementById('exportContent');
showExportTab('prompt');
}
function showExportTab(tabId) {
document.querySelectorAll('.export-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.export-tab').forEach(t => {
if (t.textContent.toLowerCase().includes(tabId === 'prompt' ? 'prompt' : tabId === 'pipeline' ? 'pipeline' : tabId === 'brief' ? 'brief' : 'json')) {
t.classList.add('active');
}
});
const container = document.getElementById('exportContent');
if (tabId === 'prompt') container.innerHTML = generatePromptTab();
else if (tabId === 'pipeline') container.innerHTML = generatePipelineTab();
else if (tabId === 'brief') container.innerHTML = generateBriefTab();
else if (tabId === 'json') container.innerHTML = generateJSONTab();
}
function generatePromptTab() {
const prompt = generatePromptText();
return `
<div class="export-pane active">
<div class="export-output" id="promptExport">${escapeHtml(prompt)}</div>
<div class="export-actions">
<button class="btn-copy" onclick="copyToClipboard(generatePromptText())">Kopier prompt</button>
</div>
</div>
`;
}
function generatePromptText() {
if (state.cart.length === 0) return 'Ingen kapabiliteter valgt.';
const scenario = state.activeScenario ? SCENARIOS.find(s => s.id === state.activeScenario) : null;
const orgName = state.intake.orgType ? (ORG_TYPE_NAMES[state.intake.orgType] || state.intake.orgType) : '';
const totalCost = getCartTotalCost();
const grouped = groupCartByAisle();
const licenses = getCartLicenses();
let prompt = '';
if (scenario) prompt += `Jeg planlegger en losning for "${scenario.name}"`;
else prompt += 'Jeg planlegger en Microsoft AI-losning';
if (orgName) prompt += ` for en ${orgName.toLowerCase()}`;
if (state.intake.target) prompt += ` rettet mot ${state.intake.target.replace('-', ' ')}`;
prompt += '.\n';
if (state.intake.users) prompt += `Antall brukere: ${state.intake.users}.\n`;
if (state.intake.volume) prompt += `Estimert volum: ${state.intake.volume}.\n`;
if (state.intake.timeframe) prompt += `Tidsramme: ${state.intake.timeframe}.\n`;
const licList = licenses.map(l => LICENSE_NAMES[l] || l).join(', ');
prompt += `Antatte lisenser: ${licList}.\n`;
prompt += `Estimert kostnad: ~${Math.round(totalCost * NOK_RATE).toLocaleString()} NOK/maned (~$${totalCost.toLocaleString()} USD).\n`;
if (state.intake.residency) prompt += `Dataresidens: ${state.intake.residency === 'norway' ? 'Norway East' : state.intake.residency === 'eu' ? 'EU Data Boundary' : 'Global'}.\n`;
if (state.intake.compliance.length > 0) prompt += `Compliance-krav: ${state.intake.compliance.join(', ')}.\n`;
if (state.intake.aiActLevel) {
prompt += `EU AI Act risikoniva: ${state.intake.aiActLevel}.\n`;
if (state.intake.aiActRole) prompt += `Rolle: ${state.intake.aiActRole}.\n`;
if (state.intake.aiActLevel === 'high-risk') {
prompt += 'NB: Hoyrisiko — FRIA (Art. 27) og samsvarsvurdering (Art. 43) kreves.\n';
}
}
prompt += '\nValgte kapabiliteter:\n';
Object.keys(grouped).forEach(aisleName => {
grouped[aisleName].forEach(item => {
const sources = item.sources.map(s => BRANDS[s].name).join(' + ');
prompt += `- [${aisleName}] ${item.name} (${sources})`;
if (item.reason) prompt += `${item.reason}`;
prompt += '\n';
});
});
if (state.intake.freetext) prompt += `\nTilleggsbeskrivelse: ${state.intake.freetext}\n`;
prompt += '\nKjor /architect:utredning med disse valgene som utgangspunkt.';
return prompt;
}
function generatePipelineTab() {
const scenarioId = state.activeScenario || 'custom';
const pipeline = COMMAND_PIPELINES[scenarioId] || COMMAND_PIPELINES['custom'];
const orgName = state.intake.orgType ? (ORG_TYPE_NAMES[state.intake.orgType] || '') : '';
let cmds = pipeline.commands.map((c, i) => {
let fullCmd = `/architect:${c.cmd}`;
if (c.args) fullCmd += ` ${c.args}`;
// Add context from intake
if (c.cmd === 'utredning' && orgName) fullCmd += `${orgName}`;
if (c.cmd === 'cost' && state.intake.users) fullCmd += ` (${state.intake.users} brukere)`;
return `
<div class="cmd-row">
<span class="cmd-num">${i + 1}</span>
<div class="cmd-info">
<div class="cmd-code">${escapeHtml(fullCmd)}</div>
<div class="cmd-desc">${c.desc}</div>
</div>
<button class="cmd-copy" onclick="copyToClipboard('${escapeAttr(fullCmd)}')">Kopier</button>
</div>
`;
}).join('');
if (state.intake.aiActLevel === 'high-risk') {
cmds += `<div style="margin:1rem 0 0.5rem;font-size:0.82rem;color:var(--text-dim);font-weight:600">EU AI Act compliance</div>`;
const aiCmds = [
{ cmd: 'classify', desc: 'Bekreft AI Act-klassifisering' },
{ cmd: 'frimpact', desc: 'FRIA — obligatorisk for offentlig sektor' },
{ cmd: 'requirements', desc: 'Konkrete deployer/provider-krav' },
{ cmd: 'conformity', desc: 'Samsvarsvurdering (Annex IV)' }
];
cmds += aiCmds.map((c, i) => `
<div class="cmd-row">
<span class="cmd-num">+${i + 1}</span>
<div class="cmd-info">
<div class="cmd-code">/architect:${c.cmd}</div>
<div class="cmd-desc">${c.desc}</div>
</div>
<button class="cmd-copy" onclick="copyToClipboard('/architect:${c.cmd}')">Kopier</button>
</div>
`).join('');
}
return `
<div class="export-pane active">
${pipeline.platforms.length > 0 ? `<div style="margin-bottom:1rem;font-size:0.85rem;color:var(--text-dim)">Plattformer: <strong style="color:var(--text)">${pipeline.platforms.join(', ')}</strong></div>` : ''}
<div class="cmd-list">${cmds}</div>
</div>
`;
}
function generateBriefTab() {
return `
<div class="export-pane active">
<div class="export-output" style="max-height:400px">${escapeHtml(generateBriefMarkdown())}</div>
<div class="export-actions">
<button class="btn-copy" onclick="copyToClipboard(generateBriefMarkdown())">Kopier</button>
<button class="btn-download" onclick="downloadFile(generateBriefMarkdown(),'azure-ai-architecture-brief.md','text/markdown')">Last ned .md</button>
</div>
</div>
`;
}
function generateBriefMarkdown() {
const scenario = state.activeScenario ? SCENARIOS.find(s => s.id === state.activeScenario) : null;
const orgName = state.intake.orgType ? (ORG_TYPE_NAMES[state.intake.orgType] || state.intake.orgType) : 'Ikke spesifisert';
const grouped = groupCartByAisle();
const totalCost = getCartTotalCost();
const userMult = getUserMultiplier();
const p50 = Math.round(totalCost * userMult * NOK_RATE);
let md = `# Azure AI Architecture Brief\n\n`;
md += `> Generert fra Azure AI Architecture Playground\n`;
md += `> Dato: ${new Date().toISOString().split('T')[0]}\n\n---\n\n`;
if (scenario) md += `## Scenario: ${scenario.name}\n\n${scenario.desc}\n\n`;
md += `**Organisasjon:** ${orgName}\n`;
md += `**Brukere:** ${state.intake.users || 'Ikke spesifisert'}\n`;
md += `**Dataresidens:** ${state.intake.residency === 'norway' ? 'Norway East' : state.intake.residency === 'eu' ? 'EU Data Boundary' : 'Global'}\n`;
if (state.intake.aiActLevel) {
md += `**EU AI Act:** ${state.intake.aiActLevel === 'high-risk' ? 'Hoyrisiko (Annex III)' : state.intake.aiActLevel === 'limited' ? 'Begrenset risiko' : state.intake.aiActLevel === 'minimal' ? 'Minimal risiko' : 'Forbudt'}\n`;
if (state.intake.aiActRole) md += `**Rolle:** ${state.intake.aiActRole}\n`;
}
md += `**Estimert kostnad:** ~${p50.toLocaleString()} NOK/mnd (P50)\n`;
md += `**Antall kapabiliteter:** ${state.cart.length}\n\n---\n\n`;
Object.keys(grouped).forEach(aisleName => {
md += `## ${aisleName}\n\n`;
grouped[aisleName].forEach(item => {
const sources = item.sources.map(s => BRANDS[s].name).join(', ');
md += `### ${item.name}\n\n`;
md += `- **Tjenester:** ${sources}\n`;
md += `- **Kostnad:** ${item.cost === 'free' ? 'Inkludert' : '~$' + item.costEst + '/mnd'}\n`;
md += `- **Skill:** ${item.skill === 'citizen' ? 'No-code' : item.skill === 'pro' ? 'Low-code' : 'Code-first'}\n`;
md += `- **Setup:** ~${item.setupDays} dager\n`;
if (item.reason) md += `- **Begrunnelse:** ${item.reason}\n`;
md += '\n';
});
});
if (state.intake.aiActLevel === 'high-risk') {
md += `## EU AI Act — Compliance-krav\n\n`;
md += `- [ ] FRIA gjennomfort (Art. 27) — /architect:frimpact\n`;
md += `- [ ] Samsvarsvurdering (Art. 43) — /architect:conformity\n`;
md += `- [ ] Transparensnotis (Art. 50) — /architect:transparency\n`;
md += `- [ ] Logging min. 6 mnd (Art. 12/26)\n`;
md += `- [ ] Menneskelig tilsyn formalisert (Art. 14)\n`;
md += `\n**Frist:** 2. august 2026 (GPAI + Annex III hoyrisiko)\n\n---\n\n`;
}
md += `## Prompt for /architect:utredning\n\n\`\`\`\n${generatePromptText()}\n\`\`\`\n`;
md += `\n---\n\n*Generert fra Azure AI Architecture Playground*\n`;
return md;
}
function generateJSONTab() {
const json = JSON.stringify(generateJSONRecord(), null, 2);
return `
<div class="export-pane active">
<div class="export-output" style="max-height:400px">${escapeHtml(json)}</div>
<div class="export-actions">
<button class="btn-copy" onclick="copyToClipboard(JSON.stringify(generateJSONRecord(),null,2))">Kopier</button>
<button class="btn-download" onclick="downloadFile(JSON.stringify(generateJSONRecord(),null,2),'azure-ai-decision-record.json','application/json')">Last ned .json</button>
</div>
</div>
`;
}
function generateJSONRecord() {
const scenario = state.activeScenario ? SCENARIOS.find(s => s.id === state.activeScenario) : null;
const totalCost = getCartTotalCost();
const userMult = getUserMultiplier();
return {
version: '2.0',
generated: new Date().toISOString(),
source: 'Azure AI Architecture Playground',
intake: {
orgType: state.intake.orgType || null,
size: state.intake.size || null,
licenses: state.intake.licenses,
compliance: state.intake.compliance,
residency: state.intake.residency,
users: state.intake.users || null,
volume: state.intake.volume || null,
budget: state.intake.budget || null,
timeframe: state.intake.timeframe || null,
target: state.intake.target || null,
freetext: state.intake.freetext || null
},
scenario: scenario ? { id: scenario.id, name: scenario.name } : null,
capabilities: state.cart.map(c => {
const item = ITEMS.find(i => i.id === c.id);
return item ? {
id: item.id,
name: item.name,
aisle: item.aisle,
sources: item.sources,
cost: item.cost,
costEstUSD: item.costEst,
license: item.license,
skill: item.skill,
setupDays: item.setupDays,
reason: c.reason || null
} : null;
}).filter(Boolean),
costEstimate: {
currency: 'NOK',
p10: Math.round(totalCost * 0.7 * NOK_RATE),
p50: Math.round(totalCost * userMult * NOK_RATE),
p90: Math.round(totalCost * 1.5 * userMult * NOK_RATE),
userMultiplier: userMult
},
aiAct: state.intake.aiActLevel ? {
riskLevel: state.intake.aiActLevel,
role: state.intake.aiActRole || null,
requiresFRIA: state.intake.aiActLevel === 'high-risk',
requiresConformity: state.intake.aiActLevel === 'high-risk',
deadline: state.intake.aiActLevel === 'high-risk' ? '2026-08-02' : null
} : null,
pipeline: (COMMAND_PIPELINES[state.activeScenario] || COMMAND_PIPELINES['custom']).commands.map(c => ({
command: `/architect:${c.cmd}`,
args: c.args || null,
description: c.desc
}))
};
}
// =========================================================================
// UTILITY FUNCTIONS
// =========================================================================
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
// Flash feedback on all copy buttons briefly
const btns = document.querySelectorAll('.btn-copy, .cmd-copy');
const clicked = event?.target;
if (clicked) {
const orig = clicked.textContent;
clicked.textContent = 'Kopiert!';
clicked.style.background = 'var(--accent2)';
setTimeout(() => { clicked.textContent = orig; clicked.style.background = ''; }, 1500);
}
});
}
function downloadFile(content, filename, type) {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function escapeAttr(text) {
return text.replace(/'/g, "\\'").replace(/"/g, '\\"');
}
// =========================================================================
// BOOT
// =========================================================================
init();
</script>
</body>
</html>