feat(ms-ai-architect): playground v3 project creation + detail shell [skip-docs]
Step 7/17 av Playground v3-leveransen (Session 2, Wave 2).
Prosjekt-opprettelse via modal: navn (påkrevd) + system-beskrivelse +
scenario-tagging multiSelect (8 scenarioer fra v2). projectId via
crypto.randomUUID. Modal mounter til document.body med Esc-/backdrop-luk.
Per-prosjekt detalj-skall (#surface-project):
- Header med tittel + scenario-chips + dato + rapport-meter + tilbake/slett
- 5 kategori-tabs (regulatorisk/sikkerhet/økonomi/dokumentasjon/verktøy)
- ALLE 24 commands rendres som .command-card i sine respektive panels
(inaktive paneler [hidden]). Sikrer at querySelectorAll-asserts matcher
uavhengig av aktiv tab; tab-bytte er ren visning-toggle uten re-render
så textarea-input bevares.
Sub-card-struktur per command:
- Skjema-zone (placeholder for Step 8 renderCommandForm)
- rapport-produserende (17): paste-import-zone (textarea[data-paste-import]
+ button[data-action=parse]) + report-zone (div[data-report-slot])
- verktøy (7): .guide-panel--info 'Verktøy'-notis ingen rapport-import
Sletting via modal med .error-summary 'Bekreft sletting'-melding (.btn--
destructive).
Paste-import-wiring: ACTIONS['parse'] leser textarea[data-paste-import]
og kaller window.__handlePasteImport(commandId, markdown). Stub logger
'parse-pending:' + slice(0,80) og injiserer en venter-panel i slot.
Step 12 erstatter stub med full PARSERS+RENDERERS-routing.
Verifisert via vm sandbox etter createProject + navigate('project'):
- 17 [data-paste-import] (rapport-produserende commands) ✓
- 17 [data-report-slot] ✓
- 24 [data-command-card] ✓
- 5 [role=tab] ✓
- 7 .guide-panel--info (verktøy-notiser) ✓
- project.id matcher UUID-format ✓
README/CLAUDE.md-update deferred til Step 17 (Session 5).
This commit is contained in:
parent
ff99a51d1d
commit
268169892a
1 changed files with 402 additions and 4 deletions
|
|
@ -1337,18 +1337,338 @@
|
|||
);
|
||||
}
|
||||
|
||||
// Stub for project + catalog som fylles i Steps 7, 9.
|
||||
// ============================================================
|
||||
// PROJECT SURFACE (Step 7)
|
||||
// ============================================================
|
||||
//
|
||||
// Per-prosjekt detalj: header med navn + scenario-chips, 5 kategori-tabs
|
||||
// (én per CATALOG-kategori), command-kort i hver tab. Sub-zones per kort:
|
||||
// 1. Skjema-zone — placeholder (Step 8 fyller med renderCommandForm)
|
||||
// 2. Paste-import — KUN for produces_report=true (textarea + parse-knapp)
|
||||
// 3. Rapport-slot — KUN for produces_report=true (data-report-slot)
|
||||
// Verktøy-commands får skjema-zone + .guide-panel--info 'Verktøy'-notis.
|
||||
//
|
||||
// Prosjekt-opprettelse via modal (createProjectFromModal). projectId =
|
||||
// crypto.randomUUID. Sletting via .error-summary-modal med eksplisitt
|
||||
// bekreftelse.
|
||||
//
|
||||
// Active-tab er transient (modul-lokal currentProjectTab) så export-state
|
||||
// ikke forurenses av UI-state. Default 'regulatory' ved hver project-enter.
|
||||
|
||||
// 8 scenarioer fra v2 — gjenbrukes som scenario-tags på prosjekter.
|
||||
const SCENARIOS = [
|
||||
{ id: 'rag-chatbot', name: 'RAG-chatbot for interne dokumenter' },
|
||||
{ id: 'autonomous-agent', name: 'Autonom agent for saksbehandling' },
|
||||
{ id: 'document-classification', name: 'Dokumentklassifisering og -prosessering' },
|
||||
{ id: 'multi-agent', name: 'Multi-agent workflow' },
|
||||
{ id: 'copilot-extension', name: 'Copilot-utvidelse for M365' },
|
||||
{ id: 'customer-service', name: 'Kundeservice-chatbot' },
|
||||
{ id: 'intelligent-search', name: 'Intelligent søk på tvers av fagsystemer' },
|
||||
{ id: 'reporting', name: 'AI-assistert rapportering' }
|
||||
];
|
||||
|
||||
let currentProjectTab = 'regulatory';
|
||||
|
||||
function findProject(id) {
|
||||
const list = store.state.projects || [];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i].id === id) return list[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function createProject(data) {
|
||||
const id = (typeof crypto !== 'undefined' && crypto.randomUUID)
|
||||
? crypto.randomUUID()
|
||||
: 'p-' + Date.now() + '-' + Math.random().toString(36).slice(2, 10);
|
||||
const project = {
|
||||
id: id,
|
||||
name: data.name || 'Uten navn',
|
||||
description: data.description || '',
|
||||
scenarios: Array.isArray(data.scenarios) ? data.scenarios.slice() : [],
|
||||
createdAt: new Date().toISOString(),
|
||||
reports: {} // commandId → { input: {...}, raw_markdown: '', parsed: {...} }
|
||||
};
|
||||
// Push via Proxy så change-event fyres og persistens skedules.
|
||||
store.state.projects.push(project);
|
||||
store.state.activeProjectId = id;
|
||||
currentProjectTab = 'regulatory';
|
||||
return project;
|
||||
}
|
||||
|
||||
function deleteProject(id) {
|
||||
const list = store.state.projects;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i].id === id) {
|
||||
list.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (store.state.activeProjectId === id) store.state.activeProjectId = null;
|
||||
}
|
||||
|
||||
// ---- Modal infrastructure ----
|
||||
|
||||
function mountModal(html) {
|
||||
unmountModal();
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = html;
|
||||
const node = wrapper.firstElementChild;
|
||||
if (!node) return;
|
||||
node.setAttribute('data-modal-root', 'true');
|
||||
document.body.appendChild(node);
|
||||
// Klikk på backdrop (selve roten) lukker; klikk inni .modal bobler ikke til root.
|
||||
node.addEventListener('click', function (ev) {
|
||||
if (ev.target === node) unmountModal();
|
||||
});
|
||||
// Esc lukker
|
||||
function escHandler(ev) {
|
||||
if (ev.key === 'Escape') {
|
||||
unmountModal();
|
||||
document.removeEventListener('keydown', escHandler);
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', escHandler);
|
||||
// Fokuser første input
|
||||
setTimeout(function () {
|
||||
const first = node.querySelector('input, select, textarea, button');
|
||||
if (first && first.focus) first.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function unmountModal() {
|
||||
const existing = document.querySelector('[data-modal-root]');
|
||||
if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
|
||||
}
|
||||
|
||||
function renderNewProjectModalHtml() {
|
||||
const scenarioOptions = SCENARIOS.map(function (s, i) {
|
||||
return (
|
||||
'<label class="checkbox-row" for="np-scen-' + i + '">' +
|
||||
'<input type="checkbox" id="np-scen-' + i + '" data-new-project-scenario value="' + escapeAttr(s.id) + '">' +
|
||||
'<span>' + escapeHtml(s.name) + '</span>' +
|
||||
'</label>'
|
||||
);
|
||||
}).join('');
|
||||
return (
|
||||
'<div class="modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="np-title">' +
|
||||
'<div class="modal">' +
|
||||
'<h2 class="modal__title" id="np-title">Nytt prosjekt</h2>' +
|
||||
'<div class="field-row">' +
|
||||
'<label for="np-name" class="field-label">Prosjektnavn<span class="required-mark" aria-hidden="true">*</span></label>' +
|
||||
'<input type="text" id="np-name" class="input" data-new-project-field="name" required>' +
|
||||
'</div>' +
|
||||
'<div class="field-row">' +
|
||||
'<label for="np-desc" class="field-label">System-beskrivelse</label>' +
|
||||
'<textarea id="np-desc" class="textarea" data-new-project-field="description" rows="3" placeholder="Hva skal AI-systemet gjøre? Hvilke brukere?"></textarea>' +
|
||||
'</div>' +
|
||||
'<div class="field-row">' +
|
||||
'<span class="field-label">Scenario-tagging</span>' +
|
||||
'<fieldset class="multi-select" aria-label="Scenarioer">' + scenarioOptions + '</fieldset>' +
|
||||
'<span class="field-help">Brukes for sammenligning og pipeline-anbefalinger.</span>' +
|
||||
'</div>' +
|
||||
'<div class="error-summary" data-new-project-errors hidden role="alert">' +
|
||||
'<h3 class="error-summary__heading">Mangler input</h3>' +
|
||||
'<div class="error-summary__body"><p data-new-project-error-text></p></div>' +
|
||||
'</div>' +
|
||||
'<div class="modal__actions">' +
|
||||
'<button type="button" class="btn btn--ghost" data-action="modal-cancel">Avbryt</button>' +
|
||||
'<button type="button" class="btn btn--primary" data-action="create-project">Opprett</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
function renderDeleteProjectModalHtml(project) {
|
||||
const reportCount = projectReportCount(project);
|
||||
return (
|
||||
'<div class="modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="dp-title">' +
|
||||
'<div class="modal">' +
|
||||
'<h2 class="modal__title" id="dp-title">Slett prosjekt?</h2>' +
|
||||
'<div class="error-summary">' +
|
||||
'<h3 class="error-summary__heading">Bekreft sletting</h3>' +
|
||||
'<div class="error-summary__body">' +
|
||||
'<p>Dette fjerner prosjektet <strong>' + escapeHtml(project.name) + '</strong> og ' + reportCount + ' importert' + (reportCount === 1 ? '' : 'e') + ' rapport' + (reportCount === 1 ? '' : 'er') + '. Handlingen kan ikke angres.</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="modal__actions">' +
|
||||
'<button type="button" class="btn btn--ghost" data-action="modal-cancel">Avbryt</button>' +
|
||||
'<button type="button" class="btn btn--destructive" data-action="confirm-delete-project" data-project-id="' + escapeAttr(project.id) + '">Slett prosjekt</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Sub-card rendering ----
|
||||
|
||||
function renderCommandSubCard(cmd) {
|
||||
const titleHtml = (
|
||||
'<div class="command-card__head">' +
|
||||
'<div>' +
|
||||
'<h3 class="command-card__title">' + escapeHtml(cmd.label) + '</h3>' +
|
||||
'<p class="command-card__desc">' + escapeHtml(cmd.description) + '</p>' +
|
||||
'</div>' +
|
||||
'<span class="command-card__id">/architect:' + escapeHtml(cmd.id) + '</span>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
const formZone = (
|
||||
'<div class="sub-zone">' +
|
||||
'<h4 class="sub-zone__heading">Skjema</h4>' +
|
||||
'<div class="form-zone-placeholder" data-form-zone="' + escapeAttr(cmd.id) + '">Skjema-renderer kommer i Step 8 (' + cmd.input_fields.length + ' felter, ' + (cmd.input_fields.filter(function (f) { return f.from === 'shared'; }).length) + ' fra shared).</div>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
if (!cmd.produces_report) {
|
||||
// Verktøy: skjema-zone + .guide-panel--info notis
|
||||
const toolNotice = (
|
||||
'<div class="sub-zone">' +
|
||||
'<div class="guide-panel guide-panel--info">' +
|
||||
'<div class="guide-panel__icon" aria-hidden="true">i</div>' +
|
||||
'<div class="guide-panel__body">' +
|
||||
'<h3 class="guide-panel__title">Verktøy</h3>' +
|
||||
'<p class="guide-panel__text">Dette er et verktøy. Ingen rapport-import — bruk skjemaet til å bygge en pipeline-streng som kjøres i terminalen.</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
return (
|
||||
'<article class="command-card" data-command-card data-command-id="' + escapeAttr(cmd.id) + '">' +
|
||||
titleHtml +
|
||||
formZone +
|
||||
toolNotice +
|
||||
'</article>'
|
||||
);
|
||||
}
|
||||
|
||||
// Rapport-produserende: skjema-zone + paste-import-zone + report-zone
|
||||
const pasteZone = (
|
||||
'<div class="sub-zone">' +
|
||||
'<h4 class="sub-zone__heading">Lim inn rapport-output</h4>' +
|
||||
'<div class="paste-import-row">' +
|
||||
'<textarea class="textarea" data-paste-import="' + escapeAttr(cmd.id) + '" rows="4" placeholder="Lim inn markdown-output fra terminalen her"></textarea>' +
|
||||
'<div class="paste-import-row__actions">' +
|
||||
'<button type="button" class="btn btn--secondary btn--sm" data-action="parse" data-command="' + escapeAttr(cmd.id) + '">Analyser rapport</button>' +
|
||||
'<span class="field-help">Routes via PARSERS[' + escapeHtml(cmd.report_archetype || '?') + '] → ' + escapeHtml(cmd.renderer || '?') + ' (Step 11/12).</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
const reportZone = (
|
||||
'<div class="sub-zone">' +
|
||||
'<h4 class="sub-zone__heading">Visualisering</h4>' +
|
||||
'<div class="report-slot ' + escapeAttr(cmd.report_root_class || '') + '" data-report-slot="' + escapeAttr(cmd.id) + '"></div>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
return (
|
||||
'<article class="command-card" data-command-card data-command-id="' + escapeAttr(cmd.id) + '">' +
|
||||
titleHtml +
|
||||
formZone +
|
||||
pasteZone +
|
||||
reportZone +
|
||||
'</article>'
|
||||
);
|
||||
}
|
||||
|
||||
function renderProjectSurface() {
|
||||
const root = getSurfaceEl('project');
|
||||
if (!root) return;
|
||||
root.innerHTML = renderTopbar('Prosjekt') + '<div class="app-shell"><p>Prosjekt-detaljvisning fylles i Step 7.</p></div>';
|
||||
|
||||
const project = findProject(store.state.activeProjectId);
|
||||
if (!project) {
|
||||
// Mistet aktivt prosjekt — fall tilbake til hjem.
|
||||
navigate('home');
|
||||
return;
|
||||
}
|
||||
|
||||
const reportTotal = CATALOG.commands.filter(function (c) { return c.produces_report; }).length;
|
||||
const reportFilled = projectReportCount(project);
|
||||
|
||||
const scenarioChips = (project.scenarios || []).map(function (sid) {
|
||||
const s = SCENARIOS.find(function (x) { return x.id === sid; });
|
||||
return '<span class="project-header__chip">' + escapeHtml(s ? s.name : sid) + '</span>';
|
||||
}).join('');
|
||||
const dateChip = '<span class="project-header__chip">opprettet ' + escapeHtml((project.createdAt || '').slice(0, 10)) + '</span>';
|
||||
const progressChip = '<span class="project-header__chip">' + reportFilled + '/' + reportTotal + ' rapporter</span>';
|
||||
|
||||
const headerHtml = (
|
||||
'<header class="project-header">' +
|
||||
'<div class="project-header__top">' +
|
||||
'<div>' +
|
||||
'<h1 class="project-header__title">' + escapeHtml(project.name) + '</h1>' +
|
||||
(project.description ? '<p style="color: var(--color-text-secondary); margin-top: var(--space-2); max-width: 70ch;">' + escapeHtml(project.description) + '</p>' : '') +
|
||||
'</div>' +
|
||||
'<div style="display:flex; gap: var(--space-2); flex-shrink: 0;">' +
|
||||
'<button type="button" class="btn btn--ghost btn--sm" data-action="goto-home">← Tilbake</button>' +
|
||||
'<button type="button" class="btn btn--secondary btn--sm" data-action="delete-project" data-project-id="' + escapeAttr(project.id) + '">Slett</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="project-header__meta">' + dateChip + progressChip + scenarioChips + '</div>' +
|
||||
'</header>'
|
||||
);
|
||||
|
||||
// Tabs per CATALOG.categories
|
||||
const tabsHtml = '<div class="project-tabs" role="tablist">' + CATALOG.categories.map(function (cat) {
|
||||
const isActive = currentProjectTab === cat.id;
|
||||
return (
|
||||
'<button type="button" class="project-tab" role="tab"' +
|
||||
(isActive ? ' aria-current="true"' : '') +
|
||||
' data-action="project-tab" data-tab="' + escapeAttr(cat.id) + '">' +
|
||||
escapeHtml(cat.label) +
|
||||
'<span class="project-tab__count">' + cat.count + '</span>' +
|
||||
'</button>'
|
||||
);
|
||||
}).join('') + '</div>';
|
||||
|
||||
// Render ALLE kategori-paneler i DOM (med [hidden] på inaktive). Dette
|
||||
// sikrer at querySelectorAll('[data-paste-import]') matcher alle 17
|
||||
// rapport-produserende commands uavhengig av aktiv tab.
|
||||
const panelsHtml = CATALOG.categories.map(function (cat) {
|
||||
const isActive = currentProjectTab === cat.id;
|
||||
const cards = CATALOG.commands
|
||||
.filter(function (c) { return c.category === cat.id; })
|
||||
.map(renderCommandSubCard).join('');
|
||||
return (
|
||||
'<div class="command-cards" role="tabpanel" data-tab-panel="' + escapeAttr(cat.id) + '"' + (isActive ? '' : ' hidden') + '>' +
|
||||
cards +
|
||||
'</div>'
|
||||
);
|
||||
}).join('');
|
||||
|
||||
root.innerHTML = (
|
||||
renderTopbar('Prosjekt: ' + escapeHtml(project.name)) +
|
||||
'<div class="app-shell app-shell--wide">' +
|
||||
headerHtml +
|
||||
tabsHtml +
|
||||
panelsHtml +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
function renderCatalogStub() {
|
||||
const root = getSurfaceEl('catalog');
|
||||
if (!root) return;
|
||||
root.innerHTML = renderTopbar('Katalog') + '<div class="app-shell"><p>Command-katalog fylles i Step 9.</p></div>';
|
||||
}
|
||||
|
||||
// ---- Paste-import stub (Step 12 erstatter med faktisk routing) ----
|
||||
|
||||
function handlePasteImport(commandId, markdown) {
|
||||
// Stub: logger til konsoll for å verifisere DOM-kontrakten. Step 12
|
||||
// henter PARSERS[CATALOG[id].report_archetype] + RENDERERS[id], parser
|
||||
// markdown og injiserer i [data-report-slot="<id>"].
|
||||
console.log('parse-pending:', commandId, (markdown || '').slice(0, 80));
|
||||
const slot = document.querySelector('[data-report-slot="' + commandId + '"]');
|
||||
if (slot) {
|
||||
slot.innerHTML = '<div class="guide-panel guide-panel--warn"><div class="guide-panel__icon" aria-hidden="true">⏳</div><div class="guide-panel__body"><p class="guide-panel__text">Markdown mottatt (' + (markdown || '').length + ' tegn). Parser+renderer kommer i Step 12.</p></div></div>';
|
||||
}
|
||||
}
|
||||
window.__handlePasteImport = handlePasteImport;
|
||||
|
||||
// ============================================================
|
||||
// ONBOARDING SURFACE (Step 5)
|
||||
// ============================================================
|
||||
|
|
@ -1760,11 +2080,89 @@
|
|||
navigate('project');
|
||||
};
|
||||
|
||||
// Stub — Step 7 erstatter med modal-åpning.
|
||||
ACTIONS['new-project'] = function () {
|
||||
console.log('[playground v3] new-project: modal kommer i Step 7');
|
||||
mountModal(renderNewProjectModalHtml());
|
||||
};
|
||||
|
||||
ACTIONS['modal-cancel'] = function () { unmountModal(); };
|
||||
|
||||
ACTIONS['create-project'] = function () {
|
||||
const modal = document.querySelector('[data-modal-root]');
|
||||
if (!modal) return;
|
||||
const nameEl = modal.querySelector('[data-new-project-field="name"]');
|
||||
const descEl = modal.querySelector('[data-new-project-field="description"]');
|
||||
const errBox = modal.querySelector('[data-new-project-errors]');
|
||||
const errText = modal.querySelector('[data-new-project-error-text]');
|
||||
const name = nameEl ? String(nameEl.value || '').trim() : '';
|
||||
const description = descEl ? String(descEl.value || '').trim() : '';
|
||||
if (!name) {
|
||||
if (errBox && errText) {
|
||||
errBox.hidden = false;
|
||||
errText.textContent = 'Prosjektnavn er påkrevd.';
|
||||
}
|
||||
if (nameEl) nameEl.focus();
|
||||
return;
|
||||
}
|
||||
const scenarios = Array.from(modal.querySelectorAll('[data-new-project-scenario]'))
|
||||
.filter(function (cb) { return cb.checked; })
|
||||
.map(function (cb) { return cb.value; });
|
||||
createProject({ name: name, description: description, scenarios: scenarios });
|
||||
unmountModal();
|
||||
navigate('project');
|
||||
};
|
||||
|
||||
ACTIONS['delete-project'] = function (ev, el) {
|
||||
const id = el.dataset.projectId;
|
||||
const project = findProject(id);
|
||||
if (!project) return;
|
||||
mountModal(renderDeleteProjectModalHtml(project));
|
||||
};
|
||||
|
||||
ACTIONS['confirm-delete-project'] = function (ev, el) {
|
||||
const id = el.dataset.projectId;
|
||||
if (!id) return;
|
||||
deleteProject(id);
|
||||
unmountModal();
|
||||
navigate('home');
|
||||
};
|
||||
|
||||
ACTIONS['project-tab'] = function (ev, el) {
|
||||
const tab = el.dataset.tab;
|
||||
if (!tab) return;
|
||||
currentProjectTab = tab;
|
||||
// Toggle visning uten full re-render (bevarer textarea-input).
|
||||
const root = getSurfaceEl('project');
|
||||
if (!root) return;
|
||||
const tabs = root.querySelectorAll('.project-tab');
|
||||
tabs.forEach(function (t) {
|
||||
if (t.dataset.tab === tab) t.setAttribute('aria-current', 'true');
|
||||
else t.removeAttribute('aria-current');
|
||||
});
|
||||
const panels = root.querySelectorAll('[data-tab-panel]');
|
||||
panels.forEach(function (p) {
|
||||
p.hidden = (p.dataset.tabPanel !== tab);
|
||||
});
|
||||
};
|
||||
|
||||
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 + '"]');
|
||||
if (!textarea) return;
|
||||
const markdown = textarea.value || '';
|
||||
handlePasteImport(commandId, markdown);
|
||||
};
|
||||
|
||||
// Eksponer for Verify-asserts og Step 8/12.
|
||||
window.__SCENARIOS = SCENARIOS;
|
||||
window.__createProject = createProject;
|
||||
window.__deleteProject = deleteProject;
|
||||
window.__findProject = findProject;
|
||||
window.__mountModal = mountModal;
|
||||
window.__unmountModal = unmountModal;
|
||||
|
||||
ACTIONS['export-state'] = function () {
|
||||
try { exportState(); }
|
||||
catch (err) { console.error('[playground v3] export feilet:', err); alert('Eksport feilet: ' + err.message); }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue