ktg-plugin-marketplace/plugins/ms-ai-architect/playground/ms-ai-architect-v3.html
Kjell Tore Guttormsen 6b2ac8250e feat(ms-ai-architect): playground v3 onboarding surface (18 felles fields) [skip-docs]
Step 5/17 av Playground v3-leveransen (Session 2, Wave 2).

5 grouped sections (organization/technology/security/architecture/business)
rendered with Tier 3 .form-progress sidebar and .expansion components per
group. Validation via .error-summary with click-to-focus links.

ONBOARDING_SCHEMA mirrors agents/onboarding-agent.md Phase 1-5 (18 fields
total). commitOnboarding() writes to state.shared.<group>.<field> via
Proxy → throttled IDB/localStorage write. Re-onboard is just navigate
back to onboarding — pre-fills from state automatically.

Verified via vm sandbox: bootstrap auto-routes to onboarding when no
org.name, commitOnboarding produces >=5 keys in shared.organization,
validation catches required-empty (2) and accepts filled (0).

Surface routing: showSurface() toggles [hidden] across data-surface
sections. scheduleRender batches via queueMicrotask. Action router
dispatches data-action attributes to ACTIONS map. README/CLAUDE.md-update
deferred til Step 17 (Session 5).
2026-05-03 18:16:44 +02:00

1612 lines
77 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="nb" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ms-ai-architect — Playground v3</title>
<!-- Vendored design-system. Kilden er shared/playground-design-system/ — synces via
scripts/sync-design-system.mjs ved marketplace-rot. Aldri rediger filer under
playground/vendor/ direkte; endringer går i shared/ + re-sync. -->
<link rel="stylesheet" href="vendor/playground-design-system/fonts.css">
<link rel="stylesheet" href="vendor/playground-design-system/tokens.css">
<link rel="stylesheet" href="vendor/playground-design-system/base.css">
<link rel="stylesheet" href="vendor/playground-design-system/components.css">
<link rel="stylesheet" href="vendor/playground-design-system/components-tier2.css">
<link rel="stylesheet" href="vendor/playground-design-system/components-tier3.css">
<link rel="stylesheet" href="vendor/playground-design-system/components-tier3-supplement.css">
<!-- App-shell layout. Vendored design-system levner komponent-CSS;
her bor kun side-spesifikk layout-grid (sidebar+main, modals, sub-cards).
Kompakt med vilje — ingen komponent-CSS skal duplikeres her. -->
<style>
main#app { min-height: 100vh; padding: 0; }
.app-shell { max-width: 1200px; margin: 0 auto; padding: var(--space-6) var(--space-5); }
.app-shell--wide { max-width: 1400px; }
/* Topbar — vises på alle surfaces unntatt onboarding (uten projekt-kontekst) */
.topbar { display: flex; align-items: center; justify-content: space-between; padding: var(--space-3) var(--space-5); border-bottom: 1px solid var(--color-border-subtle); background: var(--color-surface); position: sticky; top: 0; z-index: 10; }
.topbar__brand { display: flex; align-items: center; gap: var(--space-2); font-weight: var(--font-weight-semibold); }
.topbar__brand-mark { width: 28px; height: 28px; border-radius: var(--radius-sm); background: var(--color-primary-500); color: var(--color-text-on-primary); display: inline-flex; align-items: center; justify-content: center; font-family: var(--font-family-mono); font-weight: var(--font-weight-bold); font-size: 13px; }
.topbar__nav { display: flex; gap: var(--space-2); align-items: center; }
.topbar__crumb { font-size: var(--font-size-sm); color: var(--color-text-secondary); }
.topbar__crumb a { cursor: pointer; }
/* Onboarding-layout: sidebar + main */
.onboarding-layout { display: grid; grid-template-columns: 280px 1fr; gap: var(--space-6); align-items: start; }
@media (max-width: 880px) { .onboarding-layout { grid-template-columns: 1fr; } .form-progress { position: static; width: auto; } }
.onboarding-header { margin-bottom: var(--space-5); }
.onboarding-header h1 { font-size: var(--font-size-2xl); margin: 0 0 var(--space-2); }
.onboarding-header p { color: var(--color-text-secondary); margin: 0; max-width: 60ch; }
.onboarding-groups { display: flex; flex-direction: column; gap: var(--space-3); margin-bottom: var(--space-6); }
.onboarding-fields { display: flex; flex-direction: column; gap: var(--space-4); padding: var(--space-2) 0; }
.field-row { display: flex; flex-direction: column; gap: 6px; }
.field-label { font-size: var(--font-size-sm); font-weight: var(--font-weight-medium); color: var(--color-text-primary); }
.field-help { font-size: var(--font-size-xs); color: var(--color-text-tertiary); }
.multi-select { display: flex; flex-direction: column; gap: 4px; border: 0; padding: 0; margin: 0; }
.checkbox-row { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: var(--font-size-sm); padding: 4px 0; }
.checkbox-row input { margin: 0; }
.required-mark { color: var(--color-severity-critical); margin-left: 2px; }
.onboarding-actions { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) 0; flex-wrap: wrap; }
.onboarding-help { font-size: var(--font-size-sm); color: var(--color-text-tertiary); }
/* Home + project list */
.home-hero { display: flex; flex-direction: column; gap: var(--space-2); margin-bottom: var(--space-5); }
.home-hero h1 { font-size: var(--font-size-3xl); }
.home-hero p { color: var(--color-text-secondary); }
.home-section-head { display: flex; align-items: baseline; justify-content: space-between; margin: var(--space-6) 0 var(--space-3); }
.home-section-head h2 { font-size: var(--font-size-xl); }
.home-section-head .home-section-meta { color: var(--color-text-tertiary); font-size: var(--font-size-sm); }
/* Project surface */
.project-header { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-5) 0 var(--space-4); border-bottom: 1px solid var(--color-border-subtle); margin-bottom: var(--space-5); }
.project-header__top { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-4); }
.project-header__title { font-size: var(--font-size-2xl); margin: 0; }
.project-header__meta { display: flex; flex-wrap: wrap; gap: var(--space-3); font-size: var(--font-size-sm); color: var(--color-text-secondary); }
.project-header__chip { display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px; border-radius: var(--radius-sm); background: var(--color-bg-soft); color: var(--color-text-secondary); font-size: var(--font-size-xs); font-family: var(--font-family-mono); }
.project-tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--color-border-subtle); margin-bottom: var(--space-5); flex-wrap: wrap; }
.project-tab { background: transparent; border: 0; padding: 10px 16px; cursor: pointer; font-family: inherit; font-size: var(--font-size-sm); font-weight: var(--font-weight-medium); color: var(--color-text-secondary); border-bottom: 2px solid transparent; margin-bottom: -1px; }
.project-tab:hover { color: var(--color-text-primary); }
.project-tab[aria-current="true"] { color: var(--color-text-primary); border-bottom-color: var(--color-primary-500); }
.project-tab__count { display: inline-block; margin-left: 6px; padding: 1px 6px; background: var(--color-bg-soft); border-radius: 10px; font-size: 11px; color: var(--color-text-tertiary); }
.command-cards { display: flex; flex-direction: column; gap: var(--space-4); }
.command-card { background: var(--color-surface); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-3); }
.command-card__head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3); }
.command-card__title { font-size: var(--font-size-md); font-weight: var(--font-weight-semibold); margin: 0; }
.command-card__desc { font-size: var(--font-size-sm); color: var(--color-text-secondary); margin: 4px 0 0; }
.command-card__id { font-family: var(--font-family-mono); font-size: var(--font-size-xs); color: var(--color-text-tertiary); }
.sub-zone { border-top: 1px solid var(--color-border-subtle); padding-top: var(--space-3); }
.sub-zone__heading { font-size: var(--font-size-xs); font-weight: var(--font-weight-semibold); text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-text-tertiary); margin: 0 0 var(--space-2); }
.paste-import-row { display: flex; flex-direction: column; gap: var(--space-2); }
.paste-import-row__actions { display: flex; gap: var(--space-2); align-items: center; }
.form-zone-placeholder { padding: var(--space-3); background: var(--color-bg-soft); border-radius: var(--radius-sm); font-size: var(--font-size-sm); color: var(--color-text-tertiary); font-style: italic; }
.report-slot { min-height: 24px; }
.report-slot:empty::before { content: "Ingen importert rapport ennå."; font-size: var(--font-size-sm); color: var(--color-text-tertiary); font-style: italic; }
/* Modal */
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 100; padding: var(--space-4); }
.modal { background: var(--color-surface); border-radius: var(--radius-lg); padding: var(--space-5); max-width: 560px; width: 100%; max-height: 90vh; overflow-y: auto; box-shadow: var(--shadow-lg); display: flex; flex-direction: column; gap: var(--space-4); }
.modal__title { margin: 0; font-size: var(--font-size-xl); }
.modal__actions { display: flex; gap: var(--space-2); justify-content: flex-end; padding-top: var(--space-3); border-top: 1px solid var(--color-border-subtle); }
[data-theme="dark"] .modal-backdrop { background: rgba(0,0,0,0.7); }
</style>
</head>
<body>
<!-- Walking-skeleton: 4 placeholder-overflater. Step 5-7 fyller dem ut.
Bare én av disse er aktiv om gangen via state.activeSurface. -->
<main id="app">
<section id="surface-onboarding" data-surface="onboarding" hidden></section>
<section id="surface-home" data-surface="home" hidden></section>
<section id="surface-catalog" data-surface="catalog" hidden></section>
<section id="surface-project" data-surface="project" hidden></section>
</main>
<!--
Klassisk script (ikke type="module") av to grunner:
1. External <script type="module" src="..."> feiler på file:// i Chrome+Firefox
(ref WHATWG html#8121, Chromium 41378227).
2. Single-file deployment per brief Constraints — ingen build-step.
Kommende steps utvider IIFE-en under: Step 2 (state-modul), Step 3 (eksport/import),
Step 4 (CATALOG), osv.
-->
<script>
(function () {
'use strict';
// localStorage-nøkkel og schema-versjon. Endring av STATE_KEY krever migrasjons-steg
// (se Step 3 — MIGRATIONS-pipeline). SCHEMA_VERSION bumpes ved breaking endringer
// i state-form og driver eager migrations ved import.
const STATE_KEY = 'ms-ai-architect-state-v1';
const SCHEMA_VERSION = 1;
// Eksponer som globals for Verify-asserts og DevTools-debugging. Senere steps
// utvider window.__-namespace med __store, __CATALOG, __PARSERS, __RENDERERS,
// __buildCommand, __buildEnvelope, __handlePasteImport.
window.__STATE_KEY = STATE_KEY;
window.__SCHEMA_VERSION = SCHEMA_VERSION;
// ============================================================
// STATE MODULE (Step 2)
// ============================================================
//
// Reactivity-skjelett: Proxy + EventTarget. set-trap batcher dispatchEvent
// via queueMicrotask, så N synkrone mutasjoner gir bare én 'change'-event
// per mikrotask-tick. Bruker dyp wrap (Proxy rekursivt på objekt-properties)
// så nestede oppdateringer (state.shared.organization.name = ...) fanges.
//
// Persistens: IDB primær (~ubegrenset for 1-5 MB), localStorage fallback
// (5 MiB cap). Open-DB pattern bruker Promise-wrapper og synkrone
// migrasjoner i onupgradeneeded — async cursor-iterasjon er forbudt
// (ref w3c/IndexedDB#282: korrupsjons-risiko ved async i upgrade-tx).
//
// Multi-tab: db.onversionchange = () => db.close() defensivt på alle
// koblinger så en versjon-bump i en annen tab ikke stuck-blokkerer denne.
class StateBus extends EventTarget {}
const sharedBus = new StateBus();
const projectBus = new StateBus();
// Initial state-form. Step 5+ utvider shared.* etter onboarding-skjema;
// Step 7 utvider projects[]. preferences.theme settes i Step 13.
const INITIAL_STATE = {
schemaVersion: SCHEMA_VERSION,
shared: {
organization: {},
technology: {},
security: {},
architecture: {},
business: {}
},
projects: [],
activeProjectId: null,
activeSurface: 'home',
preferences: { theme: 'dark' }
};
// Microtask-batched event dispatcher. Mange synkrone set-traps i samme
// tick → én 'change'-event neste mikrotask. Forhindrer N renders ved
// batch-mutasjoner (f.eks. import-flow).
function makeBatchedDispatcher(bus) {
let pending = false;
const changedPaths = new Set();
return function dispatch(path) {
changedPaths.add(path);
if (pending) return;
pending = true;
queueMicrotask(function () {
pending = false;
const paths = Array.from(changedPaths);
changedPaths.clear();
bus.dispatchEvent(new CustomEvent('change', { detail: { paths: paths } }));
});
};
}
// Dyp Proxy-wrap. Lazy: wrapper child-objekter ved første read, så cost
// er bare betalt for grener brukeren faktisk berører. set-trap returnerer
// boolean (Proxy spec-invariant) og dispatcher batched 'change'-event.
// Path tracking gjør at subscribers kan filtrere på relevante grener.
function deepProxy(target, dispatch, path) {
path = path || '';
const cache = new WeakMap();
const handler = {
get: function (obj, key) {
const value = obj[key];
if (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
if (cache.has(value)) return cache.get(value);
const childPath = path ? path + '.' + String(key) : String(key);
const wrapped = new Proxy(value, makeHandler(childPath));
cache.set(value, wrapped);
return wrapped;
}
if (Array.isArray(value)) {
// Array-mutasjoner via push/splice trigger set på indekser; wrap likt.
if (cache.has(value)) return cache.get(value);
const childPath = path ? path + '.' + String(key) : String(key);
const wrapped = new Proxy(value, makeHandler(childPath));
cache.set(value, wrapped);
return wrapped;
}
return value;
},
set: function (obj, key, value) {
obj[key] = value;
dispatch(path ? path + '.' + String(key) : String(key));
return true;
},
deleteProperty: function (obj, key) {
delete obj[key];
dispatch(path ? path + '.' + String(key) : String(key));
return true;
}
};
function makeHandler(p) {
return {
get: function (o, k) { return new Proxy(target, handler).constructor === Proxy ? handler.get(o, k) : o[k]; },
set: function (o, k, v) { o[k] = v; dispatch(p ? p + '.' + String(k) : String(k)); return true; },
deleteProperty: function (o, k) { delete o[k]; dispatch(p ? p + '.' + String(k) : String(k)); return true; }
};
}
return new Proxy(target, handler);
}
function createStore(initial, bus) {
const dispatch = makeBatchedDispatcher(bus);
const proxied = deepProxy(initial, dispatch, '');
return {
state: proxied,
raw: initial, // referanse til underliggende objekt — for serialisering
subscribe: function (handler) { bus.addEventListener('change', handler); },
unsubscribe: function (handler) { bus.removeEventListener('change', handler); }
};
}
// Throttled persistens: debounce 300 ms etter siste mutasjon, så bursts
// (import-flow, batch-form-submit) committer bare én gang.
function makeThrottledWriter(persist) {
let timer = null;
return function schedule() {
if (timer) clearTimeout(timer);
timer = setTimeout(function () {
timer = null;
persist().catch(function (err) {
console.error('[playground v3] persist failed:', err);
});
}, 300);
};
}
// ============================================================
// PERSISTENCE LAYER
// ============================================================
//
// IDB primær. Én DB ('ms-ai-architect-playground-v1') med to object-stores:
// - 'shared': nøkkel 'shared' → { organization, technology, ... }
// - 'projects': nøkkel 'projectId' → project-objekt
//
// Migrasjoner i onupgradeneeded er SYNKRONE per spec — ingen await på
// cursor.continue(); bruk callback-stil. async cursor-iterasjon i en
// upgrade-tx kan korruptere DB (w3c/IndexedDB#282).
function openDB(name, version) {
return new Promise(function (resolve, reject) {
if (typeof indexedDB === 'undefined') {
reject(new Error('IndexedDB ikke tilgjengelig'));
return;
}
const req = indexedDB.open(name, version);
req.onupgradeneeded = function (ev) {
const db = req.result;
const oldVersion = ev.oldVersion;
// Synkrone migrasjoner — opprette stores per oldVersion-guard.
if (oldVersion < 1) {
if (!db.objectStoreNames.contains('shared')) {
db.createObjectStore('shared');
}
if (!db.objectStoreNames.contains('projects')) {
db.createObjectStore('projects', { keyPath: 'id' });
}
if (!db.objectStoreNames.contains('meta')) {
db.createObjectStore('meta');
}
}
// Senere bump-er legges til som "if (oldVersion < N)"-blokker.
};
req.onsuccess = function () {
const db = req.result;
// Defensiv multi-tab: hvis annen tab åpner med høyere versjon,
// lukk denne så de ikke blokkerer hverandre.
db.onversionchange = function () {
db.close();
console.warn('[playground v3] IDB versionchange — closed for upgrade');
};
resolve(db);
};
req.onerror = function () { reject(req.error); };
req.onblocked = function () {
// En annen tab holder en eldre versjon åpen; usannsynlig i v3
// (én DB-versjon per release), men logg likevel.
console.warn('[playground v3] IDB open blocked — another tab holds older version');
};
});
}
// Primær persistens: IDB. Ved feil (Safari private mode, kvote-overflow)
// fallback til localStorage. Returnerer adapter med lik API — kallere
// trenger ikke vite hvilken backend som er i bruk.
async function makePersistence() {
const DB_NAME = 'ms-ai-architect-playground-v1';
const DB_VERSION = 1;
try {
const db = await openDB(DB_NAME, DB_VERSION);
return {
backend: 'idb',
load: function () {
return new Promise(function (resolve, reject) {
const tx = db.transaction(['shared', 'projects', 'meta'], 'readonly');
const sharedReq = tx.objectStore('shared').get('shared');
const projectsReq = tx.objectStore('projects').getAll();
const metaReq = tx.objectStore('meta').get('meta');
tx.oncomplete = function () {
resolve({
schemaVersion: (metaReq.result && metaReq.result.schemaVersion) || SCHEMA_VERSION,
shared: sharedReq.result || INITIAL_STATE.shared,
projects: projectsReq.result || [],
activeProjectId: (metaReq.result && metaReq.result.activeProjectId) || null,
activeSurface: (metaReq.result && metaReq.result.activeSurface) || 'home',
preferences: (metaReq.result && metaReq.result.preferences) || INITIAL_STATE.preferences
});
};
tx.onerror = function () { reject(tx.error); };
});
},
save: function (state) {
return new Promise(function (resolve, reject) {
const tx = db.transaction(['shared', 'projects', 'meta'], 'readwrite');
tx.objectStore('shared').put(state.shared, 'shared');
const projectStore = tx.objectStore('projects');
// Clear-and-rewrite er enkelt og atomær for moderate volum.
// Ved >100 prosjekter bør dette switch-es til diff-write.
projectStore.clear();
for (let i = 0; i < state.projects.length; i++) {
projectStore.put(state.projects[i]);
}
tx.objectStore('meta').put({
schemaVersion: state.schemaVersion,
activeProjectId: state.activeProjectId,
activeSurface: state.activeSurface,
preferences: state.preferences
}, 'meta');
tx.oncomplete = function () { resolve(); };
tx.onerror = function () { reject(tx.error); };
});
}
};
} catch (err) {
console.warn('[playground v3] IDB ikke tilgjengelig, faller tilbake til localStorage:', err && err.message);
return makeLocalStorageFallback();
}
}
function makeLocalStorageFallback() {
return {
backend: 'localStorage',
load: function () {
try {
const raw = localStorage.getItem(STATE_KEY);
if (!raw) return Promise.resolve(JSON.parse(JSON.stringify(INITIAL_STATE)));
return Promise.resolve(JSON.parse(raw));
} catch (err) {
console.error('[playground v3] localStorage parse-feil, returnerer initial state:', err);
return Promise.resolve(JSON.parse(JSON.stringify(INITIAL_STATE)));
}
},
save: function (state) {
try {
const payload = JSON.stringify(state);
// Cap-advarsel: localStorage 5 MiB cap. Ved ~4.5 MB warn brukeren.
if (payload.length > 4.5 * 1024 * 1024) {
console.warn('[playground v3] State nærmer seg localStorage 5 MiB cap. Bruk en moderne nettleser med IDB-støtte.');
}
localStorage.setItem(STATE_KEY, payload);
return Promise.resolve();
} catch (err) {
return Promise.reject(err);
}
}
};
}
// ============================================================
// BOOTSTRAP
// ============================================================
//
// Initialiser persistens, last lagret state, opprett store, hook opp
// throttled writer. Eksponer __store på window for Verify-asserts og
// DevTools — Step 3 utvider med __buildEnvelope, Step 4 med __CATALOG.
let store = null;
let persistence = null;
let scheduleWrite = null;
async function bootstrap() {
persistence = await makePersistence();
const loaded = await persistence.load();
// Sørg for at schemaVersion finnes (cold start kan returnere uten).
if (!loaded.schemaVersion) loaded.schemaVersion = SCHEMA_VERSION;
store = createStore(loaded, sharedBus);
scheduleWrite = makeThrottledWriter(function () {
return persistence.save(store.raw);
});
store.subscribe(function () { scheduleWrite(); });
window.__store = store;
window.__persistence = persistence;
// Initial-surface heuristikk: hvis onboarding aldri er gjort (ingen
// organisasjons-navn) og state ikke har eksplisitt valg fra forrige
// sesjon, gå til onboarding. Ellers bruk lagret activeSurface.
const orgName = store.state.shared && store.state.shared.organization && store.state.shared.organization.name;
if (!orgName) store.state.activeSurface = 'onboarding';
else if (!store.state.activeSurface) store.state.activeSurface = 'home';
scheduleRender();
}
// ============================================================
// EXPORT / IMPORT (Step 3)
// ============================================================
//
// Brukeren kan eksportere hele state som JSON-fil og re-importere på en
// annen enhet (eller etter localStorage.clear()). Format er en envelope
// med schemaVersion + appId — så fremtidige versjoner kan lese gamle
// eksporter via MIGRATIONS-pipeline.
//
// File System Access API krever HTTPS (secure context) og er ikke
// tilgjengelig på file:// — vi bruker Blob + URL.createObjectURL +
// <a download> for eksport, og <input type="file"> + File.text() for
// import. Begge fungerer på file:// i alle target-browsers.
//
// MIGRATIONS er en eager pipeline: ved import (eller cold-load fra
// gammel state) kjøres alle migrasjoner sekvensielt fra fil-versjon til
// gjeldende SCHEMA_VERSION. Aldri hopp over et steg — selv tomme
// migrasjoner skal være registrert (no-op) for å bevise at hoppet er
// håndtert.
const APP_ID = 'ms-ai-architect-playground';
function buildEnvelope() {
// Snapshot av rå state. JSON.stringify(JSON.parse(...)) sørger for
// at Proxy-er er stripped; vi vil ikke at envelopet skal beholde
// wrapper-referanser.
const snapshot = store ? JSON.parse(JSON.stringify(store.raw)) : JSON.parse(JSON.stringify(INITIAL_STATE));
return {
appId: APP_ID,
schemaVersion: snapshot.schemaVersion || SCHEMA_VERSION,
exportedAt: new Date().toISOString(),
shared: snapshot.shared,
projects: snapshot.projects,
activeProjectId: snapshot.activeProjectId,
activeSurface: snapshot.activeSurface,
preferences: snapshot.preferences
};
}
function exportState() {
const envelope = buildEnvelope();
const json = JSON.stringify(envelope, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const stamp = envelope.exportedAt.replace(/[:.]/g, '-');
a.href = url;
a.download = APP_ID + '-' + stamp + '.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// revokeObjectURL etter at klikket har skjedd; setTimeout 0 er nok i
// alle target-browsers (Blob URL holdes så lenge nedlastningen står
// for å initieres).
setTimeout(function () { URL.revokeObjectURL(url); }, 0);
return envelope;
}
// MIGRATIONS-pipeline. Nøkkel-format: 'N->M' der N og M er fortløpende
// SCHEMA_VERSION-tall. Funksjon tar et state-objekt og returnerer et
// nytt state-objekt på neste versjon. Aldri muter input.
//
// Når SCHEMA_VERSION bumpes til 2: legg til '1->2'-funksjon som
// transformerer v1-state til v2-form. importState plukker opp
// migrasjonen automatisk.
const MIGRATIONS = {
// Eksempel for fremtid (no-op stub):
// '1->2': function (state) { return Object.assign({}, state, { schemaVersion: 2 }); }
};
function migrateState(state) {
let current = state;
let from = current.schemaVersion || 1;
while (from < SCHEMA_VERSION) {
const key = from + '->' + (from + 1);
const fn = MIGRATIONS[key];
if (!fn) {
throw new Error('[playground v3] mangler migrasjon ' + key + ' — kan ikke trygt oppgradere import-fil');
}
current = fn(current);
if (current.schemaVersion !== from + 1) {
throw new Error('[playground v3] migrasjon ' + key + ' satte ikke schemaVersion til ' + (from + 1));
}
from = current.schemaVersion;
}
return current;
}
async function importState(file) {
// file er File-objekt fra <input type="file"> change-event.
// file.text() er Promise<string> — fungerer på file:// uten secure context.
const text = await file.text();
let envelope;
try {
envelope = JSON.parse(text);
} catch (err) {
throw new Error('Ugyldig JSON: ' + err.message);
}
if (envelope.appId !== APP_ID) {
throw new Error('Fil-en er ikke en ' + APP_ID + '-eksport (appId=' + envelope.appId + ')');
}
if (typeof envelope.schemaVersion !== 'number') {
throw new Error('Mangler schemaVersion i envelope');
}
// Migrer envelope opp til gjeldende SCHEMA_VERSION før vi commit-er.
const migrated = migrateState({
schemaVersion: envelope.schemaVersion,
shared: envelope.shared || INITIAL_STATE.shared,
projects: envelope.projects || [],
activeProjectId: envelope.activeProjectId || null,
activeSurface: envelope.activeSurface || 'home',
preferences: envelope.preferences || INITIAL_STATE.preferences
});
// Skriv direkte til persistens for å unngå at debounce-vinduet
// svelger import-en ved en samtidig page-unload.
if (persistence) {
await persistence.save(migrated);
}
// Erstatt store-state in-place. Vi kan ikke bytte ut store.raw
// sin referanse fordi Proxy-en er bundet til den; muter feltvis.
const target = store.raw;
target.schemaVersion = migrated.schemaVersion;
target.shared = migrated.shared;
target.projects = migrated.projects;
target.activeProjectId = migrated.activeProjectId;
target.activeSurface = migrated.activeSurface;
target.preferences = migrated.preferences;
// Trigger en change-event manuelt så subscribers re-rendrer.
sharedBus.dispatchEvent(new CustomEvent('change', { detail: { paths: ['*'] } }));
return migrated;
}
// Eksponer for UI-handlere (Step 5+) og DevTools-debugging.
window.__buildEnvelope = buildEnvelope;
window.__exportState = exportState;
window.__importState = importState;
window.__MIGRATIONS = MIGRATIONS;
// ============================================================
// COMMAND CATALOG (Step 4)
// ============================================================
//
// Kanonisk single-source-of-truth for alle 24 commands. Driver:
// - Step 5/8: skjema-render via input_fields[]
// - Step 9: katalog-UI gruppert på category
// - Step 11: parser-routing via report_archetype
// - Step 12: renderer-routing via renderer-feltet
// - __buildCommand: pipeline-string-bygging per command
//
// Felles-state-felter har from='shared' + shared_path='group.field'
// (oppslag mot state.shared.<group>.<field>). Lokale felter har
// from='local' og lagres i project.reports[id].input.
//
// Verktøy-commands (architect, help, research, diagram, onboard,
// generate-skills, export) har produces_report=false og null for
// archetype/root/renderer — Step 11/12 hopper over dem.
const FIELD_TYPES = {
TEXT: 'text',
TEXTAREA: 'textarea',
SELECT: 'select',
MULTI_SELECT: 'multiSelect',
BOOLEAN: 'boolean',
NUMBER: 'number'
};
// Felles felt-shorthands. Holder CATALOG kompakt og sikrer at samme
// felles-felt har eksakt samme label/type på tvers av alle commands
// som bruker det.
const SHARED = {
organisation_name: { id: 'organisation_name', label: 'Virksomhet', type: 'text', from: 'shared', shared_path: 'organization.name' },
sector: { id: 'sector', label: 'Sektor', type: 'select', from: 'shared', shared_path: 'organization.sector', options: ['Statlig', 'Kommunal', 'Fylkeskommune', 'Helseforetak', 'Undervisning', 'Annet'] },
regulatory_requirements: { id: 'regulatory_requirements', label: 'Regulatoriske krav', type: 'multiSelect', from: 'shared', shared_path: 'organization.regulatory_requirements', options: ['Personopplysningsloven/GDPR', 'Sikkerhetsloven', 'Arkivloven', 'Forvaltningsloven', 'Offentleglova', 'Helseregisterloven', 'Annet'] },
cloud_platform: { id: 'cloud_platform', label: 'Skyplattform', type: 'multiSelect', from: 'shared', shared_path: 'technology.cloud_platform', options: ['Azure', 'M365', 'Power Platform', 'On-prem', 'Hybrid', 'Annet'] },
license_type: { id: 'license_type', label: 'Lisenstype', type: 'select', from: 'shared', shared_path: 'technology.license_type', options: ['E3', 'E5', 'F1/F3', 'A3/A5', 'G3/G5', 'Annet'] },
ai_services_in_use: { id: 'ai_services_in_use', label: 'AI-tjenester i bruk', type: 'multiSelect', from: 'shared', shared_path: 'technology.ai_services_in_use', options: ['Azure OpenAI', 'Copilot for M365', 'Copilot Studio', 'AI Builder', 'Azure AI Search', 'Azure AI Services', 'Ingen', 'Annet'] },
data_classification: { id: 'data_classification', label: 'Dataklassifisering', type: 'multiSelect', from: 'shared', shared_path: 'security.data_classification', options: ['Åpen', 'Intern', 'Fortrolig', 'Strengt fortrolig', 'Hemmelig'] },
dpia_practice: { id: 'dpia_practice', label: 'DPIA-praksis i organisasjonen', type: 'select', from: 'shared', shared_path: 'security.dpia_practice', options: ['Systematisk', 'Ad hoc', 'Ikke etablert', 'Usikker'] },
annual_ai_budget: { id: 'annual_ai_budget', label: 'Årlig AI-budsjett', type: 'select', from: 'shared', shared_path: 'architecture.annual_ai_budget', options: ['<500k', '500k-2M', '2M-10M', '>10M', 'Ikke definert'] }
};
const PLATFORMS = ['Azure AI Foundry', 'Copilot Studio', 'M365 Copilot', 'Power Automate', 'AI Builder', 'Azure OpenAI', 'Azure AI Search', 'Annet'];
const RISK_LEVELS = ['minimal', 'limited', 'high', 'forbidden', 'ukjent'];
const ORG_ROLES = ['provider', 'deployer', 'distributor', 'importer', 'usikker'];
const CATALOG = {
version: '1.0',
generated_for_schema: SCHEMA_VERSION,
categories: [
{ id: 'regulatory', label: 'Regulatorisk', count: 6 },
{ id: 'security', label: 'Sikkerhet', count: 3 },
{ id: 'economy', label: 'Økonomi', count: 2 },
{ id: 'documentation', label: 'Dokumentasjon', count: 6 },
{ id: 'tool', label: 'Verktøy', count: 7 }
],
commands: [
// ===== REGULATORY (6) =====
{
id: 'classify',
category: 'regulatory',
label: 'EU AI Act — Klassifisering',
description: 'Klassifiser AI-system etter EU AI Act-risikonivå (forbidden/high/limited/minimal) og bestem rolle.',
argument_hint: '[system-beskrivelse]',
calls_agent: 'ai-act-assessor',
kb_files: ['ai-act-classification-methodology.md', 'ai-act-annex-iii-checklist.md', 'ai-act-compliance-guide.md'],
produces_report: true,
report_archetype: 'aiact',
report_root_class: 'pyramide',
renderer: 'renderAiActPyramid',
input_fields: [
SHARED.organisation_name,
SHARED.sector,
{ id: 'system_name', label: 'Systemnavn', type: 'text', from: 'local' },
{ id: 'system_description', label: 'Systembeskrivelse', type: 'textarea', from: 'local' },
{ id: 'users', label: 'Brukere', type: 'text', from: 'local' },
{ id: 'interaction_type', label: 'Interaksjonstype', type: 'select', from: 'local', options: ['chatbot', 'beslutningsstøtte', 'automatisering', 'anbefaling', 'annet'] },
{ id: 'data_sources', label: 'Datakilder', type: 'textarea', from: 'local' },
{ id: 'risk_level_assumption', label: 'Risk-level (forhåndsvurdering)', type: 'select', from: 'local', options: RISK_LEVELS }
]
},
{
id: 'requirements',
category: 'regulatory',
label: 'EU AI Act — Krav per risiko + rolle',
description: 'Konkrete AI Act-forpliktelser basert på klassifisering og rolle (provider/deployer).',
argument_hint: '[system-beskrivelse el. klassifisering]',
calls_agent: 'ai-act-assessor',
kb_files: ['ai-act-provider-obligations.md', 'ai-act-deployer-obligations.md', 'ai-act-microsoft-tools-mapping.md'],
produces_report: true,
report_archetype: 'requirements-list',
report_root_class: 'findings',
renderer: 'renderRequirements',
input_fields: [
SHARED.organisation_name,
SHARED.sector,
{ id: 'system_name', label: 'Systemnavn', type: 'text', from: 'local' },
{ id: 'system_description', label: 'Systembeskrivelse', type: 'textarea', from: 'local' },
{ id: 'risk_classification', label: 'Risikoklassifisering', type: 'select', from: 'local', options: RISK_LEVELS },
{ id: 'org_role', label: 'Rolle', type: 'select', from: 'local', options: ORG_ROLES }
]
},
{
id: 'transparency',
category: 'regulatory',
label: 'Transparensnotis (Art. 13/50)',
description: 'Generer Art. 13/50-transparensnotis på norsk for AI-system.',
argument_hint: '[system-beskrivelse]',
calls_agent: 'ai-act-assessor',
kb_files: ['ai-act-transparency-notices.md'],
produces_report: true,
report_archetype: 'text-document',
report_root_class: 'markdown-fallback',
renderer: 'renderTransparency',
input_fields: [
SHARED.organisation_name,
SHARED.sector,
{ id: 'system_name', label: 'Systemnavn', type: 'text', from: 'local' },
{ id: 'system_description', label: 'Systembeskrivelse', type: 'textarea', from: 'local' },
{ id: 'interaction_type', label: 'Interaksjonstype', type: 'select', from: 'local', options: ['chatbot', 'beslutningsstøtte', 'automatisering', 'anbefaling', 'annet'] },
{ id: 'target_audience', label: 'Målgruppe', type: 'text', from: 'local' },
{ id: 'risk_classification', label: 'Risikoklassifisering', type: 'select', from: 'local', options: RISK_LEVELS }
]
},
{
id: 'frimpact',
category: 'regulatory',
label: 'FRIA (Art. 27)',
description: 'Fundamental Rights Impact Assessment — obligatorisk for offentlig sektor som deployer.',
argument_hint: '[system-beskrivelse]',
calls_agent: 'ai-act-assessor',
kb_files: ['ai-act-fria-template.md', 'ai-act-deployer-obligations.md'],
produces_report: true,
report_archetype: 'fria',
report_root_class: 'rights-matrix',
renderer: 'renderFria',
input_fields: [
SHARED.organisation_name,
SHARED.sector,
{ id: 'system_name', label: 'Systemnavn', type: 'text', from: 'local' },
{ id: 'system_description', label: 'Systembeskrivelse', type: 'textarea', from: 'local' },
{ id: 'affected_groups', label: 'Berørte grupper', type: 'textarea', from: 'local' },
{ id: 'decisions_affected', label: 'Beslutninger som påvirkes', type: 'textarea', from: 'local' },
{ id: 'risk_classification', label: 'Risikoklassifisering', type: 'select', from: 'local', options: RISK_LEVELS }
]
},
{
id: 'conformity',
category: 'regulatory',
label: 'Samsvarsvurdering (Art. 43)',
description: 'Annex IV-sjekkliste og EU-erklæring for høyrisiko AI-systemer.',
argument_hint: '[system-beskrivelse]',
calls_agent: 'ai-act-assessor',
kb_files: ['ai-act-conformity-assessment.md', 'ai-act-provider-obligations.md'],
produces_report: true,
report_archetype: 'conformity-checklist',
report_root_class: 'findings',
renderer: 'renderConformity',
input_fields: [
SHARED.organisation_name,
SHARED.sector,
{ id: 'system_name', label: 'Systemnavn', type: 'text', from: 'local' },
{ id: 'system_description', label: 'Systembeskrivelse', type: 'textarea', from: 'local' },
{ id: 'risk_classification', label: 'Risikoklassifisering', type: 'select', from: 'local', options: RISK_LEVELS },
{ id: 'org_role', label: 'Rolle', type: 'select', from: 'local', options: ORG_ROLES },
{ id: 'existing_documentation', label: 'Eksisterende dokumentasjon', type: 'textarea', from: 'local' }
]
},
{
id: 'dpia',
category: 'regulatory',
label: 'DPIA / PVK',
description: 'Personvernkonsekvensvurdering for AI-system med risikomatrise og tiltakstabell.',
argument_hint: '[system-beskrivelse]',
calls_agent: 'dpia-agent',
kb_files: ['dpia-norwegian-methodology-ai.md', 'gdpr-compliance-ai-systems.md', 'ai-impact-assessment-framework.md'],
produces_report: true,
report_archetype: 'matrix-risk',
report_root_class: 'matrix',
renderer: 'renderDpia',
input_fields: [
SHARED.organisation_name,
SHARED.sector,
SHARED.dpia_practice,
{ id: 'system_name', label: 'Systemnavn', type: 'text', from: 'local' },
{ id: 'system_description', label: 'Systembeskrivelse', type: 'textarea', from: 'local' },
{ id: 'personal_data_types', label: 'Typer personopplysninger', type: 'textarea', from: 'local' },
{ id: 'data_subjects', label: 'Registrerte (data subjects)', type: 'text', from: 'local' },
{ id: 'legal_basis', label: 'Behandlingsgrunnlag (GDPR Art. 6)', type: 'select', from: 'local', options: ['Samtykke', 'Avtale', 'Rettslig forpliktelse', 'Vitale interesser', 'Allmenn interesse', 'Berettiget interesse'] },
{ id: 'data_sources', label: 'Datakilder', type: 'textarea', from: 'local' }
]
},
// ===== SECURITY (3) =====
{
id: 'security',
category: 'security',
label: 'Sikkerhetsvurdering (6×5)',
description: 'Sikkerhetsvurdering på 6 dimensjoner med 1-5 score, OWASP LLM Top 10.',
argument_hint: '[plattform] for [bruksscenario]',
calls_agent: 'security-assessment-agent',
kb_files: ['security-scoring-rubrics-6x5.md', 'ai-security-scoring-framework.md', 'ai-threat-modeling-stride.md'],
produces_report: true,
report_archetype: 'matrix-risk-6x5',
report_root_class: 'matrix',
renderer: 'renderSecurity',
input_fields: [
SHARED.organisation_name,
SHARED.sector,
SHARED.cloud_platform,
SHARED.data_classification,
{ id: 'platform', label: 'Plattform', type: 'select', from: 'local', options: PLATFORMS },
{ id: 'use_case', label: 'Bruksscenario', type: 'textarea', from: 'local' },
{ id: 'citizen_facing', label: 'Eksponert for innbyggere?', type: 'boolean', from: 'local' },
{ id: 'data_sources', label: 'Datakilder', type: 'textarea', from: 'local' }
]
},
{
id: 'ros',
category: 'security',
label: 'ROS-analyse (NS 5814 / ISO 31000)',
description: 'Risiko- og sårbarhetsanalyse med 7 dimensjoner og 49-trussel-bibliotek.',
argument_hint: '[system-beskrivelse] [--quick]',
calls_agent: 'ros-analysis-agent',
kb_files: ['ros-ai-threat-library.md', 'ros-scoring-rubrics-7x5.md', 'ros-methodology-ns5814-iso31000.md'],
produces_report: true,
report_archetype: 'matrix-risk',
report_root_class: 'matrix',
renderer: 'renderRos',
input_fields: [
SHARED.organisation_name,
SHARED.sector,
{ id: 'system_name', label: 'Systemnavn', type: 'text', from: 'local' },
{ id: 'system_description', label: 'Systembeskrivelse', type: 'textarea', from: 'local' },
{ id: 'complexity', label: 'Kompleksitet', type: 'select', from: 'local', options: ['ENKEL', 'MIDDELS', 'KOMPLEKS'] },
{ id: 'quick_mode', label: 'Hurtig-modus (mal A)', type: 'boolean', from: 'local' }
]
},
{
id: 'review',
category: 'security',
label: 'Arkitekturgjennomgang',
description: 'Gjennomgang mot Digdir, AI Act, NSM, Schrems II og norsk offentlig sektor-krav.',
argument_hint: '[arkitektur el. kontekst]',
calls_agent: 'architecture-review-agent',
kb_files: ['decision-trees.md', 'security.md', 'public-sector-checklist.md'],
produces_report: true,
report_archetype: 'findings',
report_root_class: 'findings',
renderer: 'renderReview',
input_fields: [
SHARED.organisation_name,
SHARED.sector,
{ id: 'architecture_description', label: 'Arkitekturbeskrivelse', type: 'textarea', from: 'local' },
{ id: 'review_stage', label: 'Stadium', type: 'select', from: 'local', options: ['Pre-implementering', 'POC', 'Produksjon'] }
]
},
// ===== ECONOMY (2) =====
{
id: 'cost',
category: 'economy',
label: 'Kostnadsestimat (P10/P50/P90 NOK)',
description: 'Kostnadsestimering med konfidensgradering og TCO-sammenligning.',
argument_hint: '[plattform] med [antall brukere], [volum/dag]',
calls_agent: 'cost-estimation-agent',
kb_files: ['deterministic-cost-calculation-model.md', 'azure-ai-foundry-cost-governance.md', 'cost-models.md'],
produces_report: true,
report_archetype: 'cost-distribution',
report_root_class: 'distribution',
renderer: 'renderCost',
input_fields: [
SHARED.organisation_name,
SHARED.license_type,
SHARED.cloud_platform,
{ id: 'platform', label: 'Plattform', type: 'select', from: 'local', options: PLATFORMS },
{ id: 'users', label: 'Antall brukere', type: 'number', from: 'local' },
{ id: 'volume_per_day', label: 'Volum per dag (transaksjoner/forespørsler)', type: 'text', from: 'local' },
{ id: 'region', label: 'Region', type: 'select', from: 'local', options: ['Norge (Norway East/West)', 'EU/EØS', 'Globalt'] }
]
},
{
id: 'license',
category: 'economy',
label: 'Lisens → AI-kapabiliteter',
description: 'Map lisenstype mot inkluderte AI-kapabiliteter og identifiser gap.',
argument_hint: '[lisenstype]',
calls_agent: 'license-mapper-agent',
kb_files: ['licensing-matrix.md'],
produces_report: true,
report_archetype: 'capability',
report_root_class: 'capability-matrix',
renderer: 'renderLicense',
input_fields: [
SHARED.organisation_name,
SHARED.license_type,
SHARED.ai_services_in_use,
{ id: 'license_types', label: 'Lisenser å vurdere', type: 'multiSelect', from: 'local', options: ['E3', 'E5', 'F1/F3', 'A3/A5', 'G3/G5', 'Copilot for M365', 'Power Platform Premium'] }
]
},
// ===== DOCUMENTATION (6) =====
{
id: 'migrate',
category: 'documentation',
label: 'Migreringsplan',
description: 'Plan for migrasjon mellom Microsoft AI-plattformer.',
argument_hint: 'fra [kilde] til [mål]',
calls_agent: null,
kb_files: ['migration-patterns.md'],
produces_report: true,
report_archetype: 'phased-plan',
report_root_class: 'aiact-timeline',
renderer: 'renderMigrate',
input_fields: [
SHARED.organisation_name,
{ id: 'source_platform', label: 'Fra (kildeplattform)', type: 'select', from: 'local', options: PLATFORMS },
{ id: 'target_platform', label: 'Til (målplattform)', type: 'select', from: 'local', options: PLATFORMS },
{ id: 'system_description', label: 'Systembeskrivelse', type: 'textarea', from: 'local' },
{ id: 'timeline_weeks', label: 'Tidslinje (uker)', type: 'number', from: 'local' }
]
},
{
id: 'adr',
category: 'documentation',
label: 'ADR (MADR v3.0)',
description: 'Architecture Decision Record i MADR v3.0-format.',
argument_hint: '[valgfritt: tittel]',
calls_agent: 'adr-writer-agent',
kb_files: ['adr-template.md'],
produces_report: true,
report_archetype: 'markdown',
report_root_class: 'markdown-fallback',
renderer: 'renderAdr',
input_fields: [
SHARED.organisation_name,
{ id: 'decision_title', label: 'Beslutningstittel', type: 'text', from: 'local' },
{ id: 'decision_context', label: 'Kontekst', type: 'textarea', from: 'local' },
{ id: 'alternatives', label: 'Alternativer vurdert', type: 'textarea', from: 'local' },
{ id: 'decision', label: 'Valgt løsning', type: 'textarea', from: 'local' }
]
},
{
id: 'summary',
category: 'documentation',
label: 'Teknisk sammendrag + beslutningsnotat',
description: 'Aggregerer .work/-rapporter til teknisk sammendrag og beslutningsnotat.',
argument_hint: '[løsningsnavn]',
calls_agent: 'summary-agent',
kb_files: [],
produces_report: true,
report_archetype: 'verdict',
report_root_class: 'verdict-block',
renderer: 'renderSummary',
input_fields: [
SHARED.organisation_name,
{ id: 'solution_name', label: 'Løsningsnavn', type: 'text', from: 'local' }
]
},
{
id: 'poc',
category: 'documentation',
label: 'POC-plan',
description: 'POC-plan med suksesskriterier, tidslinje, risiko og Go/No-Go.',
argument_hint: '[plattform] for [use case]',
calls_agent: null,
kb_files: ['poc-template.md'],
produces_report: true,
report_archetype: 'phased-plan',
report_root_class: 'pipeline-cockpit',
renderer: 'renderPoc',
input_fields: [
SHARED.organisation_name,
SHARED.sector,
SHARED.annual_ai_budget,
{ id: 'platform', label: 'Plattform', type: 'select', from: 'local', options: PLATFORMS },
{ id: 'use_case', label: 'Use case', type: 'textarea', from: 'local' },
{ id: 'team_size', label: 'Team-størrelse', type: 'number', from: 'local' },
{ id: 'team_level', label: 'Team-nivå', type: 'select', from: 'local', options: ['Junior', 'Mid', 'Senior', 'Mixed'] },
{ id: 'timeline_weeks', label: 'Tidslinje (uker)', type: 'number', from: 'local' },
{ id: 'stakeholders', label: 'Interessenter', type: 'textarea', from: 'local' }
]
},
{
id: 'utredning',
category: 'documentation',
label: 'AI-arkitekturutredning (off. sektor)',
description: 'Full S0S9 arkitekturutredning for norsk offentlig sektor.',
argument_hint: '[scenario]',
calls_agent: null,
kb_files: ['ai-utredning-template.md'],
produces_report: true,
report_archetype: 'markdown',
report_root_class: 'markdown-fallback',
renderer: 'renderUtredning',
input_fields: [
SHARED.organisation_name,
SHARED.sector,
SHARED.regulatory_requirements,
{ id: 'scenario_name', label: 'Scenario-navn', type: 'text', from: 'local' },
{ id: 'scenario_description', label: 'Scenario-beskrivelse', type: 'textarea', from: 'local' },
{ id: 'system_description', label: 'Systembeskrivelse', type: 'textarea', from: 'local' }
]
},
{
id: 'compare',
category: 'documentation',
label: 'Sammenlign plattformer',
description: 'Side-by-side sammenligning av Microsoft AI-plattformer for et use case.',
argument_hint: '[plattform A] vs [plattform B] for [use case]',
calls_agent: 'research-agent',
kb_files: ['decision-trees.md'],
produces_report: true,
report_archetype: 'comparison',
report_root_class: 'diff',
renderer: 'renderCompare',
input_fields: [
SHARED.organisation_name,
{ id: 'platform_a', label: 'Plattform A', type: 'select', from: 'local', options: PLATFORMS },
{ id: 'platform_b', label: 'Plattform B', type: 'select', from: 'local', options: PLATFORMS },
{ id: 'use_case', label: 'Use case', type: 'textarea', from: 'local' }
]
},
// ===== TOOL (7) — ingen rapport, kun skjema + output-kopiering =====
{
id: 'architect',
category: 'tool',
label: 'Start Cosmo-rådgivning',
description: 'Start strukturert AI-arkitekturrådgivning med Cosmo Skyberg-persona.',
argument_hint: '[beskriv ditt forretningsproblem]',
calls_agent: null,
kb_files: [],
produces_report: false,
report_archetype: null,
report_root_class: null,
renderer: null,
input_fields: [
SHARED.organisation_name,
SHARED.sector,
{ id: 'business_problem', label: 'Forretningsproblem', type: 'textarea', from: 'local' }
]
},
{
id: 'help',
category: 'tool',
label: 'Hjelp',
description: 'Vis kommando-/agent-/KB-oversikt eller detaljer for et emne.',
argument_hint: '[emne for detaljer]',
calls_agent: null,
kb_files: [],
produces_report: false,
report_archetype: null,
report_root_class: null,
renderer: null,
input_fields: [
{ id: 'topic', label: 'Emne (valgfritt)', type: 'text', from: 'local' }
]
},
{
id: 'research',
category: 'tool',
label: 'Plattform-research',
description: 'Siste-nytt-research for en Microsoft AI-plattform.',
argument_hint: '[plattformnavn] [tidsperiode]',
calls_agent: 'research-agent',
kb_files: [],
produces_report: false,
report_archetype: null,
report_root_class: null,
renderer: null,
input_fields: [
{ id: 'platform', label: 'Plattform', type: 'select', from: 'local', options: PLATFORMS },
{ id: 'time_period', label: 'Tidsperiode', type: 'select', from: 'local', options: ['siste uke', 'siste måned', 'siste kvartal', 'siste år'] }
]
},
{
id: 'diagram',
category: 'tool',
label: 'Generer arkitekturdiagram',
description: 'Generer arkitekturdiagram med Imagen 3 (mcp-image).',
argument_hint: '[type] for [scenario]',
calls_agent: 'diagram-generation-agent',
kb_files: ['diagram-prompt-templates.md'],
produces_report: false,
report_archetype: null,
report_root_class: null,
renderer: null,
input_fields: [
{ id: 'diagram_type', label: 'Diagramtype', type: 'select', from: 'local', options: ['arkitektur', 'sikkerhet', 'dataflyt', 'problem', 'roadmap'] },
{ id: 'scenario', label: 'Scenario', type: 'text', from: 'local' },
{ id: 'component_list', label: 'Komponenter (valgfritt)', type: 'textarea', from: 'local' }
]
},
{
id: 'onboard',
category: 'tool',
label: 'Onboard plugin',
description: 'Onboard pluginen med virksomhetsspesifikk kontekst (5-fase intervju).',
argument_hint: '[--status]',
calls_agent: 'onboarding-agent',
kb_files: [],
produces_report: false,
report_archetype: null,
report_root_class: null,
renderer: null,
input_fields: [
{ id: 'status_only', label: 'Bare vis status', type: 'boolean', from: 'local' }
]
},
{
id: 'generate-skills',
category: 'tool',
label: 'Generer KB-filer (batch)',
description: 'Generer kunnskapsfiler med MCP-research i batch.',
argument_hint: '[antall]',
calls_agent: null,
kb_files: [],
produces_report: false,
report_archetype: null,
report_root_class: null,
renderer: null,
input_fields: [
{ id: 'count', label: 'Antall filer å generere', type: 'number', from: 'local' }
]
},
{
id: 'export',
category: 'tool',
label: 'Eksporter til PDF',
description: 'Eksporter et arkitekturdokument til PDF.',
argument_hint: '[filsti til markdown]',
calls_agent: null,
kb_files: [],
produces_report: false,
report_archetype: null,
report_root_class: null,
renderer: null,
input_fields: [
{ id: 'file_path', label: 'Filsti til markdown', type: 'text', from: 'local' }
]
}
]
};
// Eksponer for Step 5/8/9/11/12 og DevTools.
window.__CATALOG = CATALOG;
window.__SHARED_FIELDS = SHARED;
window.__FIELD_TYPES = FIELD_TYPES;
// ============================================================
// DOM HELPERS
// ============================================================
function escapeHtml(str) {
return String(str == null ? '' : str)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function escapeAttr(str) { return escapeHtml(str); }
// ============================================================
// SURFACE ROUTING (Step 5)
// ============================================================
//
// Én [data-surface] er synlig om gangen, drevet av state.activeSurface.
// navigate(name) muterer state og scheduler render. scheduleRender batcher
// via queueMicrotask så flere mutasjoner i samme tick gir én render.
//
// Vi subscriber IKKE alle state-endringer til render — det ville
// re-rendret skjemaer mens brukeren skriver. Render trigges eksplisitt
// fra action-handlers og navigate().
function getSurfaceEl(name) {
return document.querySelector('[data-surface="' + name + '"]');
}
function showSurface(name) {
const surfaces = document.querySelectorAll('main#app > [data-surface]');
for (let i = 0; i < surfaces.length; i++) {
surfaces[i].hidden = (surfaces[i].dataset.surface !== name);
}
}
let renderQueued = false;
function scheduleRender() {
if (renderQueued) return;
renderQueued = true;
queueMicrotask(function () {
renderQueued = false;
renderActive();
});
}
function renderActive() {
if (!store) return;
const active = store.state.activeSurface || 'home';
showSurface(active);
if (active === 'onboarding') renderOnboardingSurface();
else if (active === 'home') renderHomeSurface();
else if (active === 'project') renderProjectSurface();
else if (active === 'catalog') renderCatalogStub();
}
function navigate(surface) {
store.state.activeSurface = surface;
scheduleRender();
}
// Stubber for surfaces som fylles i Steps 6, 7, 9. Holder renderActive
// total uten å kreve at de finnes.
function renderHomeSurface() {
const root = getSurfaceEl('home');
if (!root) return;
root.innerHTML = '<div class="app-shell"><p>Hjem-skjerm fylles i Step 6.</p></div>';
}
function renderProjectSurface() {
const root = getSurfaceEl('project');
if (!root) return;
root.innerHTML = '<div class="app-shell"><p>Prosjekt-overflate fylles i Step 7.</p></div>';
}
function renderCatalogStub() {
const root = getSurfaceEl('catalog');
if (!root) return;
root.innerHTML = '<div class="app-shell"><p>Command-katalog fylles i Step 9.</p></div>';
}
// ============================================================
// ONBOARDING SURFACE (Step 5)
// ============================================================
//
// 18 felles felter strukturert i 5 grupper per agents/onboarding-agent.md
// Phase 1-5. Sidebar = .form-progress med count utfylte felter per gruppe.
// Hver gruppe = .expansion (Tier 3 supplement). Validering bruker
// .error-summary (Tier 3) med klikkbare links som fokuserer feil-felt.
//
// Lagring: commitOnboarding() muterer state.shared.<group>.<field>;
// Proxy-set-trap dispatcher 'change' → throttled writer persisterer
// til IDB. Re-onboard er bare navigate('onboarding') igjen — skjemaet
// pre-fylles automatisk fra eksisterende state.
const ONBOARDING_SCHEMA = [
{
id: 'organization',
title: 'Virksomhetsprofil',
sub: 'Hvem er dere?',
fields: [
{ id: 'name', label: 'Virksomhetsnavn', type: 'text', required: true },
{ id: 'description', label: 'Beskrivelse', type: 'textarea' },
{ id: 'sector', label: 'Sektor', type: 'select', required: true,
options: ['Statlig', 'Kommunal', 'Fylkeskommune', 'Helseforetak', 'Undervisning', 'Annet'] },
{ id: 'size', label: 'Antall ansatte', type: 'select',
options: ['<100', '100-500', '500-2000', '2000-10000', '>10000'] },
{ id: 'regulatory_requirements', label: 'Regulatoriske krav', type: 'multiSelect',
options: ['Personopplysningsloven/GDPR', 'Sikkerhetsloven', 'Arkivloven', 'Forvaltningsloven', 'Offentleglova', 'Helseregisterloven', 'Annet'] }
]
},
{
id: 'technology',
title: 'Teknologistack',
sub: 'Hva har dere allerede?',
fields: [
{ id: 'cloud_platform', label: 'Skyplattform', type: 'multiSelect',
options: ['Azure', 'M365', 'Power Platform', 'On-prem', 'Hybrid', 'Annet'] },
{ id: 'license_type', label: 'Lisenstype', type: 'select',
options: ['E3', 'E5', 'F1/F3', 'A3/A5', 'G3/G5', 'Annet'] },
{ id: 'ai_services_in_use', label: 'AI-tjenester i bruk', type: 'multiSelect',
options: ['Azure OpenAI', 'Copilot for M365', 'Copilot Studio', 'AI Builder', 'Azure AI Search', 'Azure AI Services', 'Ingen', 'Annet'] }
]
},
{
id: 'security',
title: 'Sikkerhet og compliance',
sub: 'Hvilke krav styrer dere etter?',
fields: [
{ id: 'data_classification', label: 'Dataklassifisering', type: 'multiSelect',
options: ['Åpen', 'Intern', 'Fortrolig', 'Strengt fortrolig', 'Hemmelig'] },
{ id: 'data_residency', label: 'Dataresidens-krav', type: 'select',
options: ['Norge', 'Norden', 'EU/EØS', 'Ingen spesifikke krav'] },
{ id: 'dpia_practice', label: 'DPIA-praksis', type: 'select',
options: ['Systematisk', 'Ad hoc', 'Ikke etablert', 'Usikker'] },
{ id: 'certifications', label: 'Sertifiseringer/rammeverk', type: 'textarea' }
]
},
{
id: 'architecture',
title: 'Arkitekturbeslutninger',
sub: 'Hvor vil dere?',
fields: [
{ id: 'preferred_platform', label: 'Foretrukket AI-plattform', type: 'select',
options: ['Azure AI Foundry', 'Copilot Studio', 'Power Platform/AI Builder', 'Semantic Kernel', 'Ikke bestemt'] },
{ id: 'integration_needs', label: 'Integrasjonsbehov', type: 'multiSelect',
options: ['M365', 'SharePoint', 'Dynamics 365', 'SAP', 'Fagsystemer', 'REST API-er', 'Annet'] },
{ id: 'annual_ai_budget', label: 'Årlig AI-budsjett', type: 'select',
options: ['<500k', '500k-2M', '2M-10M', '>10M', 'Ikke definert'] }
]
},
{
id: 'business',
title: 'Forretningsreferanser',
sub: 'Hvordan styrer dere?',
fields: [
{ id: 'governance_model', label: 'Styringsmodell for AI', type: 'select',
options: ['Sentralisert', 'Desentralisert', 'Hybrid', 'Ikke etablert'] },
{ id: 'doc_format_preferences', label: 'Dokumentformat', type: 'multiSelect',
options: ['Markdown', 'Word', 'PDF', 'Confluence', 'SharePoint Wiki', 'Annet'] },
{ id: 'reference_architecture', label: 'Referansearkitektur', type: 'textarea' }
]
}
];
function fieldFilled(value, type) {
if (value == null) return false;
if (type === 'multiSelect') return Array.isArray(value) && value.length > 0;
if (type === 'boolean') return value === true;
return String(value).trim() !== '';
}
function getOnboardingValue(groupId, fieldId) {
const grp = store.state.shared && store.state.shared[groupId];
if (!grp) return undefined;
return grp[fieldId];
}
function groupProgress(group) {
let filled = 0;
for (let i = 0; i < group.fields.length; i++) {
const f = group.fields[i];
if (fieldFilled(getOnboardingValue(group.id, f.id), f.type)) filled++;
}
return { filled: filled, total: group.fields.length };
}
function renderOnboardingField(field, fieldId, groupId, value) {
const path = groupId + '.' + field.id;
const dataAttrs = 'data-onboarding-field="' + escapeAttr(path) + '"';
const requiredMark = field.required ? '<span class="required-mark" aria-hidden="true">*</span>' : '';
const labelHtml = '<label for="' + fieldId + '" class="field-label">' + escapeHtml(field.label) + requiredMark + '</label>';
let inputHtml = '';
if (field.type === 'text') {
inputHtml = '<input type="text" id="' + fieldId + '" ' + dataAttrs + ' value="' + escapeAttr(value || '') + '" class="input">';
} else if (field.type === 'textarea') {
inputHtml = '<textarea id="' + fieldId + '" ' + dataAttrs + ' class="textarea" rows="3">' + escapeHtml(value || '') + '</textarea>';
} else if (field.type === 'select') {
const opts = ['<option value="">(velg)</option>'].concat(field.options.map(function (o) {
const sel = (o === value) ? ' selected' : '';
return '<option value="' + escapeAttr(o) + '"' + sel + '>' + escapeHtml(o) + '</option>';
})).join('');
inputHtml = '<select id="' + fieldId + '" ' + dataAttrs + ' class="select">' + opts + '</select>';
} else if (field.type === 'multiSelect') {
const arr = Array.isArray(value) ? value : [];
const opts = field.options.map(function (o, i) {
const checked = arr.indexOf(o) >= 0 ? ' checked' : '';
const cbId = fieldId + '-' + i;
return '<label class="checkbox-row" for="' + cbId + '"><input type="checkbox" id="' + cbId + '" ' + dataAttrs + ' data-multi-option="' + escapeAttr(o) + '"' + checked + '><span>' + escapeHtml(o) + '</span></label>';
}).join('');
inputHtml = '<fieldset class="multi-select" aria-labelledby="' + fieldId + '-legend"><legend id="' + fieldId + '-legend" class="visually-hidden">' + escapeHtml(field.label) + '</legend>' + opts + '</fieldset>';
} else if (field.type === 'boolean') {
const checked = value === true ? ' checked' : '';
inputHtml = '<label class="checkbox-row" for="' + fieldId + '"><input type="checkbox" id="' + fieldId + '" ' + dataAttrs + checked + '><span>' + escapeHtml(field.label) + '</span></label>';
}
return '<div class="field-row" data-field-row="' + escapeAttr(path) + '">' + labelHtml + inputHtml + '</div>';
}
function renderOnboardingSurface() {
const root = getSurfaceEl('onboarding');
if (!root) return;
const progress = ONBOARDING_SCHEMA.map(function (g) {
const p = groupProgress(g);
return { id: g.id, title: g.title, filled: p.filled, total: p.total };
});
const totalFilled = progress.reduce(function (a, p) { return a + p.filled; }, 0);
const totalAll = ONBOARDING_SCHEMA.reduce(function (a, g) { return a + g.fields.length; }, 0);
const sidebarSteps = progress.map(function (p, idx) {
let state = 'pending';
if (p.filled === p.total) state = 'done';
else if (p.filled > 0) state = 'in-progress';
const pct = p.total ? Math.round(100 * p.filled / p.total) : 0;
const numHtml = (state === 'done' ? '✓' : String(idx + 1));
return (
'<button type="button" class="fp-step" data-state="' + state + '" data-action="onboarding-goto-group" data-group="' + escapeAttr(p.id) + '">' +
'<span class="fp-step__num" aria-hidden="true">' + numHtml + '</span>' +
'<span>' +
'<span class="fp-step__name">' + escapeHtml(p.title) + '</span>' +
'<span class="fp-step__progress">' +
'<span class="fp-step__bar"><span class="fp-step__bar-fill" style="width:' + pct + '%"></span></span>' +
'<span>' + p.filled + '/' + p.total + '</span>' +
'</span>' +
'</span>' +
'</button>'
);
}).join('');
const sidebar = (
'<aside class="form-progress" aria-label="Onboarding-fremdrift">' +
'<div class="form-progress__autosave">' +
'<span class="form-progress__autosave-dot"></span>' +
'<span>Lagres automatisk</span>' +
'</div>' +
'<div class="form-progress__steps">' + sidebarSteps + '</div>' +
'<div class="form-progress__remaining">' +
'<span>Utfylt</span>' +
'<span>' + totalFilled + '/' + totalAll + '</span>' +
'</div>' +
'</aside>'
);
const groupsHtml = ONBOARDING_SCHEMA.map(function (g) {
const p = groupProgress(g);
const expandedAttr = (p.filled < p.total ? 'true' : 'false');
const fieldsHtml = g.fields.map(function (f) {
const fieldId = 'ob-' + g.id + '-' + f.id;
const value = getOnboardingValue(g.id, f.id);
return renderOnboardingField(f, fieldId, g.id, value);
}).join('');
return (
'<section class="expansion" aria-expanded="' + expandedAttr + '" data-onboarding-group="' + escapeAttr(g.id) + '">' +
'<button type="button" class="expansion__head" data-action="onboarding-toggle-group">' +
'<span class="expansion__title">' +
'<span class="expansion__title-main">' + escapeHtml(g.title) + '</span>' +
'<span class="expansion__title-sub">' + escapeHtml(g.sub) + ' — ' + p.filled + '/' + p.total + '</span>' +
'</span>' +
'<span class="expansion__chev" aria-hidden="true">▾</span>' +
'</button>' +
'<div class="expansion__body">' +
'<div class="expansion__body-inner">' +
'<div class="onboarding-fields">' + fieldsHtml + '</div>' +
'</div>' +
'</div>' +
'</section>'
);
}).join('');
const errorSummary = (
'<div class="error-summary" data-onboarding-errors hidden role="alert" aria-live="polite">' +
'<h2 class="error-summary__heading">Noen felter må fylles ut</h2>' +
'<div class="error-summary__body">' +
'<ul class="error-summary__list" data-onboarding-error-list></ul>' +
'</div>' +
'</div>'
);
const orgName = store.state.shared.organization && store.state.shared.organization.name;
const skipBackBtn = orgName
? '<button type="button" class="btn btn--ghost" data-action="onboarding-cancel">Tilbake til hjem</button>'
: '';
const actionBar = (
'<div class="onboarding-actions">' +
'<button type="button" class="btn btn--primary" data-action="onboarding-save">Lagre og fortsett</button>' +
skipBackBtn +
'<span class="onboarding-help">Du kan endre alt senere via Re-onboard.</span>' +
'</div>'
);
root.innerHTML = (
'<div class="app-shell">' +
'<div class="onboarding-layout">' +
sidebar +
'<div class="onboarding-main">' +
'<header class="onboarding-header">' +
'<h1>Velkommen til ms-ai-architect</h1>' +
'<p>Fyll inn 18 felles felter — de gjenbrukes på tvers av alle commands og forhåndsutfyller skjemaer per prosjekt.</p>' +
'</header>' +
errorSummary +
'<div class="onboarding-groups">' + groupsHtml + '</div>' +
actionBar +
'</div>' +
'</div>' +
'</div>'
);
}
function readOnboardingValues() {
const values = {};
ONBOARDING_SCHEMA.forEach(function (g) { values[g.id] = {}; });
const root = getSurfaceEl('onboarding');
if (!root) return values;
const fields = root.querySelectorAll('[data-onboarding-field]');
// Initialiser alle multiSelect-felter til [] så uavkryssede arrays
// blir tomme arrays (ikke undefined).
ONBOARDING_SCHEMA.forEach(function (g) {
g.fields.forEach(function (f) {
if (f.type === 'multiSelect') values[g.id][f.id] = [];
});
});
for (let i = 0; i < fields.length; i++) {
const el = fields[i];
const path = el.dataset.onboardingField;
const dot = path.indexOf('.');
const groupId = path.slice(0, dot);
const fieldId = path.slice(dot + 1);
if (el.matches('input[type="checkbox"][data-multi-option]')) {
if (el.checked) values[groupId][fieldId].push(el.dataset.multiOption);
} else if (el.matches('input[type="checkbox"]')) {
values[groupId][fieldId] = el.checked;
} else {
values[groupId][fieldId] = el.value;
}
}
return values;
}
function validateOnboarding(values) {
const errors = [];
ONBOARDING_SCHEMA.forEach(function (g) {
g.fields.forEach(function (f) {
if (!f.required) return;
const v = values[g.id][f.id];
if (!fieldFilled(v, f.type)) {
errors.push({
path: g.id + '.' + f.id,
label: g.title + ' → ' + f.label,
message: 'Påkrevd felt mangler verdi'
});
}
});
});
return errors;
}
function showOnboardingErrors(errors) {
const root = getSurfaceEl('onboarding');
if (!root) return;
const summary = root.querySelector('[data-onboarding-errors]');
const list = root.querySelector('[data-onboarding-error-list]');
if (!summary || !list) return;
if (errors.length === 0) {
summary.hidden = true;
list.innerHTML = '';
return;
}
summary.hidden = false;
list.innerHTML = errors.map(function (e) {
return '<li class="error-summary__item"><a href="#" class="error-summary__link" data-action="onboarding-focus-error" data-error-target="' + escapeAttr(e.path) + '">' + escapeHtml(e.label) + ' — ' + escapeHtml(e.message) + '</a></li>';
}).join('');
summary.scrollIntoView({ behavior: 'smooth', block: 'start' });
summary.focus && summary.focus();
}
function commitOnboarding(values) {
// Muter via Proxy så change-events fyres og throttled writer persisterer.
ONBOARDING_SCHEMA.forEach(function (g) {
if (!store.state.shared[g.id]) store.state.shared[g.id] = {};
g.fields.forEach(function (f) {
const v = values[g.id][f.id];
if (f.type === 'multiSelect') {
store.state.shared[g.id][f.id] = Array.isArray(v) ? v.slice() : [];
} else {
store.state.shared[g.id][f.id] = v;
}
});
});
}
// ============================================================
// ACTION ROUTER
// ============================================================
//
// Én delegert click-handler på document. Mapper data-action til
// registrerte handlers. Surfaces og modaler kan registrere actions ved
// å sette window.__ACTIONS[name] = function(ev, el) { ... }.
const ACTIONS = {};
window.__ACTIONS = ACTIONS;
document.addEventListener('click', function (ev) {
const actionEl = ev.target.closest('[data-action]');
if (!actionEl) return;
const action = actionEl.dataset.action;
const handler = ACTIONS[action];
if (handler) handler(ev, actionEl);
});
ACTIONS['onboarding-toggle-group'] = function (ev, el) {
const exp = el.closest('.expansion');
if (!exp) return;
const open = exp.getAttribute('aria-expanded') === 'true';
exp.setAttribute('aria-expanded', open ? 'false' : 'true');
};
ACTIONS['onboarding-goto-group'] = function (ev, el) {
const groupId = el.dataset.group;
const root = getSurfaceEl('onboarding');
if (!root) return;
const exp = root.querySelector('[data-onboarding-group="' + groupId + '"]');
if (exp) {
exp.setAttribute('aria-expanded', 'true');
exp.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
ACTIONS['onboarding-save'] = function (ev) {
const values = readOnboardingValues();
const errors = validateOnboarding(values);
if (errors.length > 0) {
showOnboardingErrors(errors);
return;
}
commitOnboarding(values);
navigate('home');
};
ACTIONS['onboarding-cancel'] = function () {
navigate('home');
};
ACTIONS['onboarding-focus-error'] = function (ev, el) {
ev.preventDefault();
const path = el.dataset.errorTarget;
const root = getSurfaceEl('onboarding');
if (!root || !path) return;
const fieldRow = root.querySelector('[data-field-row="' + path + '"]');
if (!fieldRow) return;
const exp = fieldRow.closest('.expansion');
if (exp) exp.setAttribute('aria-expanded', 'true');
fieldRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
const input = fieldRow.querySelector('input, select, textarea');
if (input) input.focus();
};
// Eksponer for Verify-asserts og Steps 6-9.
window.__navigate = navigate;
window.__scheduleRender = scheduleRender;
window.__ONBOARDING_SCHEMA = ONBOARDING_SCHEMA;
window.__readOnboardingValues = readOnboardingValues;
window.__validateOnboarding = validateOnboarding;
window.__commitOnboarding = commitOnboarding;
// Auto-bootstrap. Kjør så snart DOM er parsed; vi er på slutten av <body>
// så DOM er allerede klar.
bootstrap().catch(function (err) {
console.error('[playground v3] bootstrap failed:', err);
});
})();
</script>
</body>
</html>