From f55a0e9513efc373eaaca32ebc3ffc92b7a9368a Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Sun, 3 May 2026 18:33:19 +0200 Subject: [PATCH] feat(ms-ai-architect): playground v3 generic command form renderer + buildCommand [skip-docs] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 8 of v3 plan. renderCommandForm(commandId, opts) reads CATALOG[id].input_fields and emits a form with all 6 supported field types (text/textarea/select/multiSelect/boolean/number). Shared fields auto-prefill from state.shared via field.shared_path dot-lookup; local fields prefill from project.reports[id].input when opts.projectId is set. window.__buildCommand(commandId, formData) builds /architect: key="value" key="value" ... — shared fields merged first (CATALOG order), formData overrides and may include keys outside the catalog (passthrough). Empty/null/empty-array values omitted. Multi-values comma-joined inside quotes; quotes/backslashes escaped. Copy-button writes via navigator.clipboard.writeText with graceful fallback to inline preview when clipboard is blocked (file:// in some browsers). Preview-button shows the same string without copying. Replaces the form-zone-placeholder in renderCommandSubCard. All 24 command-cards in project-detail now render real forms (verified: data-command-card === 24, data-command-form === 24, copy-command buttons === 24, field-from-tag === 39, paste-import === 17, report-slot === 17, buildCommand('classify',{riskLevel:'høy'}) → '/architect:classify organisation_name="Vegvesen" sector="Statlig" riskLevel="høy"'). --- .../playground/ms-ai-architect-v3.html | 320 +++++++++++++++++- 1 file changed, 313 insertions(+), 7 deletions(-) diff --git a/plugins/ms-ai-architect/playground/ms-ai-architect-v3.html b/plugins/ms-ai-architect/playground/ms-ai-architect-v3.html index 923a463..7f70b13 100644 --- a/plugins/ms-ai-architect/playground/ms-ai-architect-v3.html +++ b/plugins/ms-ai-architect/playground/ms-ai-architect-v3.html @@ -86,9 +86,40 @@ /* 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--wide { max-width: 760px; } .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); } + + /* Command form (Step 8) */ + .command-form { display: flex; flex-direction: column; gap: var(--space-3); } + .command-form__fields { display: flex; flex-direction: column; gap: var(--space-3); } + .command-form__actions { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; padding-top: var(--space-2); border-top: 1px dashed var(--color-border-subtle); } + .command-form__hint { font-size: var(--font-size-xs); color: var(--color-text-tertiary); } + .command-form__copy-confirm { font-size: var(--font-size-xs); color: var(--color-text-secondary); } + .field-from-tag { display: inline-block; padding: 1px 6px; background: var(--color-bg-soft); color: var(--color-text-tertiary); border-radius: var(--radius-sm); font-size: 10px; font-weight: var(--font-weight-medium); margin-left: 6px; letter-spacing: 0.04em; text-transform: uppercase; } + .form-preview { padding: var(--space-3); background: var(--color-bg-soft); border-radius: var(--radius-sm); margin-top: var(--space-2); overflow-x: auto; } + .form-preview__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); } + .code-block { font-family: var(--font-family-mono); font-size: var(--font-size-xs); color: var(--color-text-primary); white-space: pre-wrap; word-break: break-all; margin: 0; } + + /* Catalog (Step 9) */ + .catalog-header { display: flex; flex-direction: column; gap: var(--space-2); margin: var(--space-3) 0 var(--space-4); } + .catalog-header h1 { font-size: var(--font-size-2xl); margin: 0; } + .catalog-header p { color: var(--color-text-secondary); margin: 0; max-width: 70ch; } + .catalog-toolbar { display: flex; gap: var(--space-3); align-items: center; margin-bottom: var(--space-4); flex-wrap: wrap; } + .catalog-toolbar .input { max-width: 480px; flex: 1 1 280px; } + .catalog-toolbar__count { font-size: var(--font-size-sm); color: var(--color-text-tertiary); } + .catalog-groups { display: flex; flex-direction: column; gap: var(--space-3); } + .catalog-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: var(--space-3); padding: var(--space-2) 0; } + .catalog-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-2); } + .catalog-card__head { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-2); } + .catalog-card__title { font-size: var(--font-size-md); margin: 0; font-weight: var(--font-weight-semibold); } + .catalog-card__desc { font-size: var(--font-size-sm); color: var(--color-text-secondary); margin: 4px 0 0; } + .catalog-card__pill { padding: 2px 8px; background: var(--color-bg-soft); color: var(--color-text-secondary); border-radius: var(--radius-sm); font-size: 10px; font-weight: var(--font-weight-medium); flex-shrink: 0; text-transform: uppercase; letter-spacing: 0.04em; } + .catalog-card__meta { display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center; } + .catalog-card__hint { font-size: var(--font-size-xs); color: var(--color-text-tertiary); font-family: var(--font-family-mono); } + .catalog-card__actions { display: flex; gap: var(--space-2); margin-top: auto; padding-top: var(--space-2); } + .catalog-tool-notice { padding: var(--space-2) var(--space-3); background: var(--color-bg-soft); border-left: 3px solid var(--color-primary-500); border-radius: var(--radius-sm); font-size: var(--font-size-xs); color: var(--color-text-secondary); } @@ -1131,6 +1162,244 @@ } function escapeAttr(str) { return escapeHtml(str); } + // ============================================================ + // COMMAND FORM RENDERER + __buildCommand (Step 8) + // ============================================================ + // + // renderCommandForm(commandId, opts) genererer HTML for ett command-skjema + // basert på CATALOG[id].input_fields. Brukes både i prosjekt-detalj + // (Step 7 form-zone) og i katalog-modal (Step 9). Felter med from='shared' + // pre-fylles fra state.shared via field.shared_path; lokale felter + // pre-fylles fra project.reports[id].input når opts.projectId er gitt. + // + // window.__buildCommand(commandId, formData) bygger '/architect: + // key="value" ...'-streng. Shared-felter merges inn først, formData + // overstyrer hvis samme nøkkel. Tomme/null-verdier hoppes over. formData + // kan inneholde nøkler som ikke finnes i CATALOG (passthrough). + + function resolveSharedPath(path) { + if (!path || !store || !store.state || !store.state.shared) return undefined; + const parts = String(path).split('.'); + let cur = store.state.shared; + for (let i = 0; i < parts.length; i++) { + if (cur == null || typeof cur !== 'object') return undefined; + cur = cur[parts[i]]; + } + return cur; + } + + function isFilledArg(v, type) { + if (v == null) return false; + if (type === 'multiSelect' || Array.isArray(v)) return Array.isArray(v) && v.length > 0; + if (type === 'boolean' || typeof v === 'boolean') return v === true; + if (type === 'number' || typeof v === 'number') return !isNaN(v); + return String(v).trim() !== ''; + } + + function serializeArgValue(v) { + if (Array.isArray(v)) { + return '"' + v.map(function (x) { return String(x).replace(/\\/g, '\\\\').replace(/"/g, '\\"'); }).join(',') + '"'; + } + if (typeof v === 'boolean') return String(v); + if (typeof v === 'number') return String(v); + const s = String(v).replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + return '"' + s + '"'; + } + + function buildCommand(commandId, formData) { + formData = formData || {}; + const cmd = (CATALOG.commands || []).find(function (c) { return c.id === commandId; }); + const args = {}; + // 1. Pre-fyll fra shared (CATALOG-definerte felles felter). + if (cmd && cmd.input_fields) { + cmd.input_fields.forEach(function (f) { + if (f.from === 'shared' && f.shared_path) { + const v = resolveSharedPath(f.shared_path); + if (isFilledArg(v, f.type)) args[f.id] = v; + } + }); + } + // 2. formData overstyrer / utvider. Tillater nøkler som ikke er i CATALOG. + Object.keys(formData).forEach(function (k) { + const v = formData[k]; + if (isFilledArg(v)) args[k] = v; + else delete args[k]; + }); + // 3. Bygg streng. Stable order: shared-felter først (i CATALOG-rekkefølge), + // så resten i insertion-order. + const orderedKeys = []; + const seen = {}; + if (cmd && cmd.input_fields) { + cmd.input_fields.forEach(function (f) { + if (Object.prototype.hasOwnProperty.call(args, f.id) && !seen[f.id]) { + orderedKeys.push(f.id); + seen[f.id] = true; + } + }); + } + Object.keys(args).forEach(function (k) { + if (!seen[k]) { + orderedKeys.push(k); + seen[k] = true; + } + }); + const parts = ['/architect:' + commandId]; + orderedKeys.forEach(function (k) { + parts.push(k + '=' + serializeArgValue(args[k])); + }); + return parts.join(' '); + } + + function renderCommandFormField(field, domId, value) { + const fromAttr = field.from === 'shared' ? 'shared' : 'local'; + const dataAttrs = 'data-cf-field="' + escapeAttr(field.id) + '" data-cf-from="' + fromAttr + '" data-cf-type="' + escapeAttr(field.type) + '"'; + const fromTag = field.from === 'shared' + ? 'felles' + : ''; + const labelHtml = ''; + let inputHtml = ''; + if (field.type === 'text') { + inputHtml = ''; + } else if (field.type === 'textarea') { + inputHtml = ''; + } else if (field.type === 'number') { + inputHtml = ''; + } else if (field.type === 'select') { + const opts = [''].concat((field.options || []).map(function (o) { + const sel = (o === value) ? ' selected' : ''; + return ''; + })).join(''); + inputHtml = ''; + } 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 = domId + '-' + i; + return ( + '' + ); + }).join(''); + inputHtml = ( + '
' + + '' + escapeHtml(field.label) + '' + + opts + + '
' + ); + } else if (field.type === 'boolean') { + const checked = value === true ? ' checked' : ''; + inputHtml = ( + '' + ); + } else { + // Ukjent type — fall tilbake til text. + inputHtml = ''; + } + return '
' + labelHtml + inputHtml + '
'; + } + + function renderCommandForm(commandId, opts) { + opts = opts || {}; + const cmd = (CATALOG.commands || []).find(function (c) { return c.id === commandId; }); + if (!cmd) { + return '

Ukjent command: ' + escapeHtml(commandId) + '

'; + } + const project = opts.projectId ? findProject(opts.projectId) : null; + const savedInput = (project && project.reports && project.reports[commandId] && project.reports[commandId].input) || {}; + const scope = opts.scope || 'p'; + + const fieldRows = (cmd.input_fields || []).map(function (f) { + const domId = 'cf-' + scope + '-' + cmd.id + '-' + f.id; + let value; + if (f.from === 'shared' && f.shared_path) { + value = resolveSharedPath(f.shared_path); + } + if (value === undefined || value === null || value === '') { + if (Object.prototype.hasOwnProperty.call(savedInput, f.id)) value = savedInput[f.id]; + } + return renderCommandFormField(f, domId, value); + }).join(''); + + const sharedCount = (cmd.input_fields || []).filter(function (f) { return f.from === 'shared'; }).length; + const fieldCount = (cmd.input_fields || []).length; + + return ( + '
' + + '
' + fieldRows + '
' + + '
' + + '' + + '' + + '' + fieldCount + ' felter (' + sharedCount + ' fra shared).' + + '' + + '
' + + '' + + '
' + ); + } + + function readCommandFormValues(formEl) { + const data = {}; + if (!formEl) return data; + const cmdId = formEl.dataset.commandForm; + const cmd = (CATALOG.commands || []).find(function (c) { return c.id === cmdId; }); + // Initialiser multiSelect til [] så uavkryssede ender opp tomme. + if (cmd && cmd.input_fields) { + cmd.input_fields.forEach(function (f) { + if (f.type === 'multiSelect') data[f.id] = []; + }); + } + const inputs = formEl.querySelectorAll('[data-cf-field]'); + for (let i = 0; i < inputs.length; i++) { + const el = inputs[i]; + const id = el.dataset.cfField; + if (el.matches('input[type="checkbox"][data-cf-multi]')) { + if (el.checked) { + if (!Array.isArray(data[id])) data[id] = []; + data[id].push(el.dataset.cfMulti); + } + } else if (el.matches('input[type="checkbox"]')) { + data[id] = el.checked; + } else if (el.matches('input[type="number"]')) { + if (el.value === '' || el.value == null) { + data[id] = null; + } else { + const n = Number(el.value); + data[id] = isNaN(n) ? null : n; + } + } else { + data[id] = el.value; + } + } + return data; + } + + function showCommandPreview(formEl, str) { + if (!formEl) return; + const box = formEl.querySelector('[data-form-preview]'); + const text = formEl.querySelector('[data-form-preview-text]'); + if (!box || !text) return; + text.textContent = str; + box.hidden = false; + } + + function flashCopyConfirm(formEl, message) { + if (!formEl) return; + const tag = formEl.querySelector('[data-copy-confirm]'); + if (!tag) return; + tag.textContent = message || 'Kopiert til utklippstavle.'; + tag.hidden = false; + clearTimeout(tag.__hideTimer); + tag.__hideTimer = setTimeout(function () { tag.hidden = true; }, 2400); + } + // ============================================================ // SURFACE ROUTING (Step 5) // ============================================================ @@ -1503,7 +1772,7 @@ // ---- Sub-card rendering ---- - function renderCommandSubCard(cmd) { + function renderCommandSubCard(cmd, projectId) { const titleHtml = ( '
' + '
' + @@ -1517,7 +1786,9 @@ const formZone = ( '
' + '

Skjema

' + - '
Skjema-renderer kommer i Step 8 (' + cmd.input_fields.length + ' felter, ' + (cmd.input_fields.filter(function (f) { return f.from === 'shared'; }).length) + ' fra shared).
' + + '
' + + renderCommandForm(cmd.id, { context: 'project', projectId: projectId, scope: 'p' }) + + '
' + '
' ); @@ -1631,7 +1902,7 @@ const isActive = currentProjectTab === cat.id; const cards = CATALOG.commands .filter(function (c) { return c.category === cat.id; }) - .map(renderCommandSubCard).join(''); + .map(function (c) { return renderCommandSubCard(c, project.id); }).join(''); return ( '
' + cards + @@ -2147,21 +2418,56 @@ ACTIONS['parse'] = function (ev, el) { const commandId = el.dataset.command; if (!commandId) return; - const root = getSurfaceEl('project'); - if (!root) return; - const textarea = root.querySelector('[data-paste-import="' + commandId + '"]'); + // Finn nærmeste paste-import textarea (project-overflate eller modal — Step 9 + // bruker ikke parse-knapp, men vi holder oss generisk via closest()). + const scope = el.closest('[data-modal-root], [data-surface]') || document; + const textarea = scope.querySelector('[data-paste-import="' + commandId + '"]'); if (!textarea) return; const markdown = textarea.value || ''; handlePasteImport(commandId, markdown); }; - // Eksponer for Verify-asserts og Step 8/12. + // ---- Step 8: copy-command + preview-command ---- + + ACTIONS['copy-command'] = function (ev, el) { + const commandId = el.dataset.command; + const formEl = el.closest('[data-command-form]'); + if (!commandId || !formEl) return; + const data = readCommandFormValues(formEl); + const cmdString = buildCommand(commandId, data); + // Vis preview alltid — clipboard kan feile på file://-protokoll i noen browsers. + showCommandPreview(formEl, cmdString); + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(cmdString).then(function () { + flashCopyConfirm(formEl, 'Kopiert til utklippstavle.'); + }).catch(function (err) { + console.warn('[playground v3] clipboard write feilet:', err); + flashCopyConfirm(formEl, 'Kunne ikke kopiere — bruk forhåndsvisningen under.'); + }); + } else { + flashCopyConfirm(formEl, 'Clipboard utilgjengelig — bruk forhåndsvisningen under.'); + } + }; + + ACTIONS['preview-command'] = function (ev, el) { + const commandId = el.dataset.command; + const formEl = el.closest('[data-command-form]'); + if (!commandId || !formEl) return; + const data = readCommandFormValues(formEl); + showCommandPreview(formEl, buildCommand(commandId, data)); + }; + + // Eksponer for Verify-asserts og Step 8/9/12. window.__SCENARIOS = SCENARIOS; window.__createProject = createProject; window.__deleteProject = deleteProject; window.__findProject = findProject; window.__mountModal = mountModal; window.__unmountModal = unmountModal; + window.__buildCommand = buildCommand; + window.__renderCommandForm = renderCommandForm; + window.__readCommandFormValues = readCommandFormValues; + window.__resolveSharedPath = resolveSharedPath; ACTIONS['export-state'] = function () { try { exportState(); }