Initial addition of ms-ai-architect plugin to the open-source marketplace. Private content excluded: orchestrator/ (Linear tooling), docs/utredning/ (client investigation), generated test reports and PDF export script. skill-gen tooling moved from orchestrator/ to scripts/skill-gen/. Security scan: WARNING (risk 20/100) — no secrets, no injection found. False positive fixed: added gitleaks:allow to Python variable reference in output-validation-grounding-verification.md line 109. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1977 lines
127 KiB
HTML
1977 lines
127 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="no">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Azure AI Architecture Playground</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0a0a0f;
|
|
--surface: #12121a;
|
|
--surface2: #1a1a2e;
|
|
--border: #2a2a3e;
|
|
--text: #e4e4ef;
|
|
--text-dim: #8888a0;
|
|
--accent: #0078d4;
|
|
--accent2: #00d4aa;
|
|
--accent3: #ff6b9d;
|
|
--accent4: #ffa726;
|
|
--accent5: #42a5f5;
|
|
--accent6: #ab47bc;
|
|
--accent7: #ef5350;
|
|
--accent8: #66bb6a;
|
|
--gradient1: linear-gradient(135deg, #0078d4, #00d4aa);
|
|
--gradient2: linear-gradient(135deg, #ff6b9d, #ffa726);
|
|
}
|
|
* { margin:0; padding:0; box-sizing:border-box; }
|
|
body { background:var(--bg); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif; line-height:1.6; }
|
|
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">🏛</div>Statlig etat<div class="card-desc">Departement, direktorat, tilsyn</div></div>
|
|
<div class="wizard-card" onclick="setOrgType(this,'kommune')"><div class="card-icon">🏠</div>Kommune<div class="card-desc">Kommune eller bydel</div></div>
|
|
<div class="wizard-card" onclick="setOrgType(this,'fylkeskommune')"><div class="card-icon">🗺</div>Fylkeskommune<div class="card-desc">Regional forvaltning</div></div>
|
|
<div class="wizard-card" onclick="setOrgType(this,'universitet')"><div class="card-icon">🎓</div>Universitet/hogskole<div class="card-desc">UH-sektor</div></div>
|
|
<div class="wizard-card" onclick="setOrgType(this,'helsevesen')"><div class="card-icon">🏥</div>Helsevesen<div class="card-desc">Sykehus, helseforetak</div></div>
|
|
<div class="wizard-card" onclick="setOrgType(this,'privat')"><div class="card-icon">🏢</div>Privat virksomhet<div class="card-desc">Naringsliv</div></div>
|
|
</div>
|
|
<div class="wizard-nav"><span></span><button class="wizard-next" onclick="nextSlide()">Neste →</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">👤</div>1-50 ansatte</div>
|
|
<div class="wizard-card" onclick="setSize(this,'50-200')"><div class="card-icon">👥</div>50-200 ansatte</div>
|
|
<div class="wizard-card" onclick="setSize(this,'200-1000')"><div class="card-icon">👫</div>200-1000 ansatte</div>
|
|
<div class="wizard-card" onclick="setSize(this,'1000-5000')"><div class="card-icon">👪</div>1000-5000 ansatte</div>
|
|
<div class="wizard-card" onclick="setSize(this,'5000+')"><div class="card-icon">🌍</div>5000+ ansatte</div>
|
|
</div>
|
|
<div class="wizard-nav"><button class="wizard-prev" onclick="prevSlide()">← Tilbake</button><button class="wizard-next" onclick="nextSlide()">Neste →</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()">← Tilbake</button><button class="wizard-next" onclick="nextSlide()">Neste →</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()">← Tilbake</button><button class="wizard-next" onclick="nextSlide()">Neste →</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()">← Tilbake</button><button class="wizard-next" onclick="nextSlide()">Neste →</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()">← Tilbake</button><button class="wizard-next" onclick="submitWizard()">Fullfar intake →</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 (<$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 (>$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)">← Tilbake til intake</button>
|
|
<button class="wizard-next" onclick="goToStep(3)">Konfigurer →</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)">← Tilbake</button>
|
|
<button class="wizard-next" onclick="goToStep(4)">Gjennomgang →</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)">← Tilbake</button>
|
|
<button class="wizard-next" onclick="goToStep(5)">Eksporter →</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)">← 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()">🛒<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()">×</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}')">×</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}')">×</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>
|