@adia-ai/a2ui-compose 0.4.5 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -12,6 +12,42 @@ generator graph.
12
12
 
13
13
  _No pending changes._
14
14
 
15
+ ## [0.4.6] - 2026-05-12
16
+
17
+ ### Changed — `core/` retirement follow-through (§64, v0.4.6)
18
+
19
+ Companion to `@adia-ai/a2ui-retrieval@0.4.6` retiring `pattern-library.js`:
20
+
21
+ - **`compose/core/pattern-export.js`** retired — no consumers post-§64 (the export shape targeted the retired pattern-library surface).
22
+ - **`compose/core/reference.js`** — updated to delegate to `composition-library` instead of the retired `pattern-library`.
23
+ - **`compose/core/generator.js`** — dropped pattern-export wiring; system-prompt path consumes compositions directly via §66's `adaptV09Component()`.
24
+
25
+ ### Changed — `buildSystemPrompt` reads canonical `.a2ui.json` sidecars (§66, v0.4.6)
26
+
27
+ Closes the catalog-drift surface that §56 explicitly deferred ("different schema shapes; restructuring `getComponentCatalog()` is its own arc"). The monolithic prompt builder now reads from the v0.9 `.a2ui.json` sidecars (generated from yamls by `npm run components`) instead of the legacy hand-maintained `_components.json` catalog.
28
+
29
+ **Root cause uncovered during audit:** `strategies/monolithic/_shared.js` (pre-§66) called `getComponentData(typeName)` returning the raw v0.9 sidecar — `{title, description, properties, x-adiaui}` (JSON Schema shape) — but the prompt builder code did `a2uiData.props ? Object.entries(a2uiData.props)`. The v0.9 shape uses `properties`, not `props`. **Every prompt was silently falling through to the legacy `_components.json` catalog** because every v0.9 lookup looked empty. The "canonical sidecar reads" path was wired but dead code.
30
+
31
+ **Fix:** new `adaptV09Component()` helper at the top of `_shared.js` (~90 LOC) normalizes the v0.9 sidecar shape to the legacy `{tag, category, description, props, events, slots, examples, aliases, keywords}` shape the rest of the prompt builder was written for. Properties map 1:1 (skipping `component` discriminator + `children` container + `const`-only fields). Type names map JSON-Schema lowercase → legacy Title-Case (`string`→`String`, `boolean`→`Boolean`, etc.).
32
+
33
+ **Empirical fidelity gain** (Button as canonical primitive):
34
+
35
+ | Source | Props surfaced |
36
+ |---|---|
37
+ | `_components.json` (legacy, before §66) | 7: `text, variant, size, disabled, block, icon, type` (`block` is stale — removed from yaml in an earlier arc but never cleaned up in the hand-maintained catalog) |
38
+ | v0.9 sidecar (canonical, after §66) | 10: `type, aria-label, color, disabled, icon, size, stretch, text, textContent, variant` |
39
+
40
+ **§66 surfaces ~30% more catalog data** for the LLM, all traceable to a yaml SoT that `npm run components` regenerates on demand. This is exactly the catalog drift §56 flagged: when the team added `Select.size` (§50) and `inputmode`/`autocomplete` (§51) to the yamls, those props went into the v0.9 sidecars but never propagated to `_components.json`. The LLM was getting a stale catalog view that excluded recent additions. After §66, the LLM gets the actual ground truth.
41
+
42
+ **Pitfalls encoded in the adapter** (5 v0.9-shape gotchas):
43
+ 1. `events` is an object `{eventName: {description}}`, not an array. Normalize to `string[]`.
44
+ 2. `slots` is also object-shaped. Same fix.
45
+ 3. `synonyms.tags` may be null/undefined. Wrap in `Array.isArray()` guard.
46
+ 4. `component` and `children` properties on every v0.9 sidecar are discriminator/container fields, not user-facing props. Skip in the adapter loop.
47
+ 5. `const` fields (e.g. `{const: 'Button'}`) are discriminator-only. Skip.
48
+
49
+ Source: `packages/a2ui/compose/strategies/monolithic/_shared.js` (+91, -2 lines, commit `afda98f5`).
50
+
15
51
  ## [0.4.5] - 2026-05-12
16
52
 
17
53
  ### Changed — GenUI overhaul prompt-engineering (§56, v0.4.5)
package/core/generator.js CHANGED
@@ -17,7 +17,6 @@ import { store, engine } from './state.js';
17
17
  import { checkIntentAlignment } from '../../retrieval/intent/intent-alignment.js';
18
18
  import { decomposeIntent, composeSubtasks } from '../../retrieval/intent/decomposer.js';
19
19
  import { getWiringCatalog } from '../../retrieval/wiring-catalog.js';
20
- import { getComponentData } from '../../retrieval/pattern-library.js';
21
20
 
22
21
  import { StubLLMAdapter } from '../../../llm/llm-stub.js';
23
22
  import { createAdapter } from '../../../llm/llm-bridge.js';
package/core/reference.js CHANGED
@@ -1,16 +1,44 @@
1
1
  /**
2
- * Reference Tools — Delegates to A003 intelligence system.
2
+ * Reference Tools — stable generative-system API surface.
3
3
  *
4
- * Thin wrappers that provide a stable generative-system API surface
5
- * over the intelligence module. Used by the generator and pipeline stages
6
- * to look up components, patterns, and domain context.
4
+ * Thin wrappers used by the monolithic engines + generator pipeline.
5
+ * §64 migrated the "pattern" surface from `pattern-library.js` (retired)
6
+ * to the zettel `composition-library.js`. Function names kept
7
+ * (searchBlocks/lookupPattern/listPatterns/searchBlocksSemantic) for
8
+ * back-compat with engine call sites; internally they hit compositions.
7
9
  */
8
10
 
9
11
  import { getCatalog, getComponent, getComponentsByCategory } from '../../retrieval/index.js';
10
12
  import { classifyIntent, assembleContext } from '../../retrieval/index.js';
11
- import { getAllPatterns, searchPatterns, getPattern, semanticSearchPatterns } from '../../retrieval/index.js';
12
13
  import { checkAllAntiPatterns } from '../../retrieval/index.js';
13
14
  import { serializeEntry } from '../../retrieval/index.js';
15
+ import {
16
+ searchAll as searchCompositions,
17
+ getComposition,
18
+ getAllCompositions,
19
+ } from '../strategies/zettel/composition-library.js';
20
+
21
+ /**
22
+ * Compositions have `tags: { purpose: [...], complexity: "", layout: "" }`
23
+ * but the monolithic engines (instant/pro/thinking) all assume the old
24
+ * pattern shape `tags: [string]` and call `.map(...)` on it. Flatten on
25
+ * the way out so legacy call sites keep working without re-templating
26
+ * every engine. Annotated chunks expose flat-but-shallow `tags`; both
27
+ * are handled.
28
+ */
29
+ function normalize(record) {
30
+ if (!record) return record;
31
+ let tags = record.tags;
32
+ if (Array.isArray(tags)) return record;
33
+ if (tags && typeof tags === 'object') {
34
+ tags = Object.values(tags)
35
+ .flat()
36
+ .filter((v) => typeof v === 'string');
37
+ } else {
38
+ tags = [];
39
+ }
40
+ return { ...record, tags };
41
+ }
14
42
 
15
43
  /**
16
44
  * Look up a single component by A2UI type name.
@@ -50,40 +78,47 @@ export async function getComponentsByGroup(category) {
50
78
  }
51
79
 
52
80
  /**
53
- * Search the pattern library by query.
81
+ * Search the composition library by query.
82
+ * Returns full composition records (rehydrated from name hits).
54
83
  * @param {string} query — Natural language or keyword query
55
84
  * @returns {object[]}
56
85
  */
57
86
  export function searchBlocks(query, { domain } = {}) {
58
- return searchPatterns(query, { domain });
87
+ const hits = searchCompositions(query);
88
+ const records = hits.map((h) => getComposition(h.name)).filter(Boolean);
89
+ const filtered = domain ? records.filter((c) => c.domain === domain) : records;
90
+ return filtered.map(normalize);
59
91
  }
60
92
 
61
93
  /**
62
- * Get a specific named pattern.
63
- * @param {string} name — Pattern name (e.g. 'stat-cards', 'user-profile')
94
+ * Get a specific named composition.
95
+ * @param {string} name — Composition name
64
96
  * @returns {object|null}
65
97
  */
66
98
  export function lookupPattern(name) {
67
- return getPattern(name);
99
+ return normalize(getComposition(name));
68
100
  }
69
101
 
70
102
  /**
71
- * Get all available patterns.
103
+ * Get all available compositions.
72
104
  * @returns {object[]}
73
105
  */
74
106
  export function listPatterns() {
75
- return getAllPatterns();
107
+ return getAllCompositions().map(normalize);
76
108
  }
77
109
 
78
110
  /**
79
- * Semantic search LLM-enhanced pattern matching.
80
- * Falls back to keyword search if no adapter provided.
111
+ * Keyword search wrapped as the legacy `{ matches }` return shape so the
112
+ * monolithic-thinking engine's call site keeps working. The historical
113
+ * LLM-driven semantic path retired with `pattern-library.js` in §64;
114
+ * compositions don't have a semantic search backend yet.
115
+ *
81
116
  * @param {string} query
82
- * @param {object} [options] — { llmAdapter, remix }
83
- * @returns {Promise<{ matches: object[], remix?: object }>}
117
+ * @param {object} [_options] — accepted for back-compat; llmAdapter/remix ignored
118
+ * @returns {Promise<{ matches: object[] }>}
84
119
  */
85
- export async function searchBlocksSemantic(query, options = {}) {
86
- return semanticSearchPatterns(query, options);
120
+ export async function searchBlocksSemantic(query, _options = {}) {
121
+ return { matches: searchBlocks(query) };
87
122
  }
88
123
 
89
124
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/a2ui-compose",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "AdiaUI A2UI compose engine — framework-agnostic. Takes natural-language intents + a catalog and produces A2UI protocol messages. Pairs with `@adia-ai/a2ui-retrieval` (intent classification, catalog lookup) and `@adia-ai/a2ui-validator` (schema + semantic checks).",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Dual-environment chunk loader — Node + browser.
3
3
  *
4
- * Mirrors the pattern in packages/a2ui/retrieval/pattern-library.js so the
5
- * gen-UI training chunks at packages/a2ui/corpus/chunks/*.json are reachable
6
- * from BOTH server-side engines (monolithic-pro via /api/generate's Node
7
- * process) AND browser-side engines (monolithic-pro called directly from
4
+ * Mirrors the dual-environment loading approach historically used by
5
+ * pattern-library.js (retired §64) so the gen-UI training chunks at
6
+ * packages/a2ui/corpus/chunks/*.json are reachable from BOTH server-side
7
+ * engines (monolithic-pro via /api/generate's Node process) AND
8
+ * browser-side engines (monolithic-pro called directly from
8
9
  * gen-ui playground's IIFE).
9
10
  *
10
11
  * The existing packages/a2ui/corpus/scripts/chunk-library.js is Node-only
@@ -12,7 +12,91 @@ import { store } from '../../core/state.js';
12
12
  import { checkIntentAlignment } from '../../../retrieval/intent/intent-alignment.js';
13
13
  import { composeSubtasks } from '../../../retrieval/intent/decomposer.js';
14
14
  import { getWiringCatalog } from '../../../retrieval/wiring-catalog.js';
15
- import { getComponentData } from '../../../retrieval/pattern-library.js';
15
+ import { getComponentData } from '../../../retrieval/component-catalog.js';
16
+
17
+ // ── v0.9 .a2ui.json sidecar → prompt-catalog adapter ─────────────
18
+ //
19
+ // §66 (v0.4.6): the `.a2ui.json` sidecars produced by `npm run components`
20
+ // are v0.9 JSON Schemas with `{title, description, properties, x-adiaui}`.
21
+ // The legacy `_components.json` catalog this prompt builder originally
22
+ // read had a flatter `{tag, category, description, props, events, slots,
23
+ // examples, aliases, keywords}` shape. This adapter normalizes the v0.9
24
+ // sidecar into the legacy shape so the rest of the prompt builder can stay
25
+ // unchanged.
26
+ //
27
+ // Before §66, this code did `a2uiData.props` — which silently returned 0
28
+ // props for every component (the v0.9 shape uses `properties`, not
29
+ // `props`), causing every prompt to fall through to the `_components.json`
30
+ // catalog. After §66, the canonical sidecar drives the prompt directly
31
+ // and `_components.json` is retired (§64/§65).
32
+ function adaptV09Component(a2uiData) {
33
+ if (!a2uiData || typeof a2uiData !== 'object') return null;
34
+ const ext = a2uiData['x-adiaui'] || {};
35
+ const properties = a2uiData.properties || {};
36
+
37
+ // Map v0.9 property → legacy prop shape. Skip the always-present
38
+ // `component` (const PascalCase name) + `children` (slot container)
39
+ // fields since they aren't user-facing props.
40
+ const props = {};
41
+ for (const [name, spec] of Object.entries(properties)) {
42
+ if (name === 'component' || name === 'children') continue;
43
+ if (spec?.const) continue; // discriminator-only fields
44
+ const out = {};
45
+ // JSON Schema type names are lowercase; legacy catalog used Title-Case.
46
+ if (spec.type) {
47
+ out.type = spec.type === 'string' ? 'String'
48
+ : spec.type === 'boolean' ? 'Boolean'
49
+ : spec.type === 'number' || spec.type === 'integer' ? 'Number'
50
+ : spec.type === 'array' ? 'Array'
51
+ : spec.type === 'object' ? 'Object'
52
+ : String(spec.type);
53
+ }
54
+ if (spec.enum) out.enum = spec.enum;
55
+ if (spec.default !== undefined) out.default = spec.default;
56
+ if (spec.description) out.description = spec.description;
57
+ props[name] = out;
58
+ }
59
+
60
+ // Events in the legacy catalog were a flat string[]. The v0.9 sidecar
61
+ // stores them as an object: `{eventName: {description}}` (verified
62
+ // across all 94 sidecars on 2026-05-12). Older code paths accepted
63
+ // either shape; normalize to a string[] of event names.
64
+ const eventsRaw = ext.events;
65
+ let events = [];
66
+ if (Array.isArray(eventsRaw)) {
67
+ events = eventsRaw.map((e) => (typeof e === 'string' ? e : e?.name)).filter(Boolean);
68
+ } else if (eventsRaw && typeof eventsRaw === 'object') {
69
+ events = Object.keys(eventsRaw);
70
+ }
71
+
72
+ // Slots — also object-shaped (`{slotName: {description, type}}`) in v0.9.
73
+ // Legacy catalog stored a string[]. Normalize.
74
+ const slotsRaw = ext.slots;
75
+ let slots = [];
76
+ if (Array.isArray(slotsRaw)) {
77
+ slots = slotsRaw.map((s) => (typeof s === 'string' ? s : s?.name)).filter(Boolean);
78
+ } else if (slotsRaw && typeof slotsRaw === 'object') {
79
+ slots = Object.keys(slotsRaw);
80
+ }
81
+
82
+ // Aliases live under `x-adiaui.synonyms.tags` in v0.9. The synonyms
83
+ // field is always an object (verified across all 94 sidecars). Surface
84
+ // the `tags` array — the prompt renders them as "Button (also: SubmitButton)".
85
+ const synonyms = (ext.synonyms && typeof ext.synonyms === 'object') ? ext.synonyms : null;
86
+ const aliases = Array.isArray(synonyms?.tags) ? synonyms.tags : null;
87
+
88
+ return {
89
+ tag: ext.tag || null,
90
+ category: ext.category || 'layout',
91
+ description: a2uiData.description || '',
92
+ props,
93
+ events,
94
+ slots,
95
+ examples: Array.isArray(ext.examples) ? ext.examples : [],
96
+ aliases,
97
+ keywords: Array.isArray(ext.keywords) ? ext.keywords : [],
98
+ };
99
+ }
16
100
 
17
101
  // Component prop catalog — loaded lazily for prompt injection
18
102
  let _componentCatalog = null;
@@ -160,8 +244,13 @@ KEY RULES:
160
244
  return Array.isArray(a) && a.length ? ` (also: ${a.join(', ')})` : '';
161
245
  };
162
246
  for (const typeName of relevantTypes) {
163
- // Prefer .a2ui.json data (new source of truth), fallback to _components.json
164
- const a2uiData = getComponentData(typeName);
247
+ // §66 (v0.4.6): .a2ui.json sidecar is the canonical source of truth.
248
+ // The raw v0.9 shape uses `properties` (JSON Schema); adapt to the
249
+ // legacy `props` shape this prompt builder was originally written
250
+ // for. Falls back to the (soon-retired) `_components.json` only
251
+ // when no sidecar exists.
252
+ const v09Raw = getComponentData(typeName);
253
+ const a2uiData = v09Raw ? adaptV09Component(v09Raw) : null;
165
254
  if (a2uiData) {
166
255
  const props = a2uiData.props ? Object.entries(a2uiData.props).map(([k, v]) => {
167
256
  let desc = k;
@@ -163,7 +163,7 @@ export async function generateInstant({ intent, executionId, storeId, analysis,
163
163
 
164
164
  engine.submitStage(execId, 'generate', {
165
165
  messages,
166
- source: bestMatch ? 'pattern-library' : 'fallback',
166
+ source: bestMatch ? 'composition-library' : 'fallback',
167
167
  confidence: bestMatch ? 0.95 : 0.4,
168
168
  });
169
169
 
@@ -62,6 +62,13 @@ function chunkToComposition(chunkDoc, sourcePath) {
62
62
  };
63
63
  }
64
64
 
65
+ /**
66
+ * Track whether the eager top-level load has run. Multiple `loadAll`
67
+ * call sites (mcp/server.js bootstrap + any other consumer) re-clear
68
+ * + re-build, which is wasted work but not incorrect.
69
+ */
70
+ let _autoLoaded = false;
71
+
65
72
  export function loadAll() {
66
73
  compositions.clear();
67
74
 
@@ -99,6 +106,7 @@ export function loadAll() {
99
106
  annotatedChunkCount++;
100
107
  });
101
108
 
109
+ _autoLoaded = true;
102
110
  return {
103
111
  compositionCount: compositions.size,
104
112
  handAuthoredCount: compositions.size - annotatedChunkCount,
@@ -106,6 +114,12 @@ export function loadAll() {
106
114
  };
107
115
  }
108
116
 
117
+ // Eager top-level load so synchronous getters (getComposition,
118
+ // getAllCompositions, searchAll) work for consumers that don't call
119
+ // loadAll themselves — test harnesses, smoke scripts, and the
120
+ // retrieval-side helpers migrated off pattern-library in §64.
121
+ if (!_autoLoaded) loadAll();
122
+
109
123
  // Back-compat shims removed in §38 — fragment-library.js → composition-library.js
110
124
  // rename. The retired `getFragment` / `getAllFragments` exports had zero
111
125
  // external callers; drop them.
@@ -1,149 +0,0 @@
1
- /**
2
- * Pattern Export — Save generated UI as reusable named patterns.
3
- *
4
- * Produces two downloadable files:
5
- * {name}.json — A2UI flat adjacency pattern (importable into pattern-library)
6
- * {name}.html — Rendered HTML snapshot from the canvas
7
- *
8
- * Also supports importing patterns back into the runtime library.
9
- */
10
-
11
- import { registerPattern } from '../../retrieval/pattern-library.js';
12
-
13
- /**
14
- * Build a pattern-library-compatible JSON object from generation results.
15
- *
16
- * @param {string} name — Pattern name (kebab-case, e.g. "my-pricing-page")
17
- * @param {object} opts
18
- * @param {object[]} opts.components — Flat adjacency array from messages[0].components
19
- * @param {string} opts.intent — Original user intent
20
- * @param {string} [opts.domain] — Domain classification (forms, data, layout, agent, navigation)
21
- * @param {string} [opts.description] — Human description (falls back to intent)
22
- * @returns {object} — Pattern object matching pattern-library format
23
- */
24
- export function buildPatternJSON(name, { components, intent, domain, description }) {
25
- const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
26
-
27
- // Extract unique component type names
28
- const componentTypes = [...new Set(
29
- components.map(c => c.component).filter(Boolean)
30
- )];
31
-
32
- // Clean components: strip internal fields
33
- const template = components.map(c => {
34
- const { _surfaceId, ...rest } = c;
35
- return rest;
36
- });
37
-
38
- return {
39
- name: slug,
40
- description: description || intent || slug,
41
- domain: domain || 'layout',
42
- components: componentTypes,
43
- template,
44
- };
45
- }
46
-
47
- /**
48
- * Save a generation as a named pattern. Downloads .json + .html files.
49
- *
50
- * @param {string} name — Pattern name
51
- * @param {object} opts
52
- * @param {object[]} opts.messages — A2UI messages array
53
- * @param {object} opts.validation — Validation result (must have score >= 95)
54
- * @param {string} opts.intent — Original user intent
55
- * @param {string} [opts.domain] — Domain classification
56
- * @param {string} [opts.canvasHTML] — Rendered HTML from canvas
57
- * @param {number} [opts.minScore=95] — Minimum score to allow save
58
- * @returns {{ json: object, html: string }} — The saved payloads
59
- */
60
- export function savePattern(name, { messages, validation, intent, domain, canvasHTML, minScore = 95 }) {
61
- if (!validation || validation.score < minScore) {
62
- throw new Error(`Score ${validation?.score ?? 0} is below minimum ${minScore} for pattern save`);
63
- }
64
-
65
- const components = messages?.[0]?.components;
66
- if (!components || !components.length) {
67
- throw new Error('No components to save');
68
- }
69
-
70
- const json = buildPatternJSON(name, { components, intent, domain });
71
-
72
- // Download .json
73
- downloadFile(`${json.name}.json`, JSON.stringify(json, null, 2), 'application/json');
74
-
75
- // Download .html (if available)
76
- const html = canvasHTML || '';
77
- if (html) {
78
- const htmlDoc = `<!DOCTYPE html>
79
- <html lang="en">
80
- <head>
81
- <meta charset="UTF-8">
82
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
83
- <title>${json.name} — A2UI Pattern</title>
84
- <style>body { font-family: system-ui, sans-serif; padding: 2rem; }</style>
85
- </head>
86
- <body>
87
- ${html}
88
- </body>
89
- </html>`;
90
- downloadFile(`${json.name}.html`, htmlDoc, 'text/html');
91
- }
92
-
93
- // Also register in the runtime library
94
- registerPattern(json);
95
-
96
- return { json, html };
97
- }
98
-
99
- /**
100
- * Import a pattern JSON object into the runtime pattern library.
101
- *
102
- * @param {object|string} patternJSON — Pattern object or JSON string
103
- * @returns {{ success: boolean, name?: string, error?: string }}
104
- */
105
- export function importPattern(patternJSON) {
106
- let pattern = patternJSON;
107
- if (typeof pattern === 'string') {
108
- try { pattern = JSON.parse(pattern); }
109
- catch { return { success: false, error: 'Invalid JSON' }; }
110
- }
111
-
112
- if (!pattern?.name) return { success: false, error: 'Missing pattern name' };
113
- if (!pattern?.template || !Array.isArray(pattern.template)) {
114
- return { success: false, error: 'Missing or invalid template array' };
115
- }
116
-
117
- const registered = registerPattern(pattern);
118
- if (!registered) {
119
- return { success: false, error: `Pattern "${pattern.name}" already exists` };
120
- }
121
-
122
- return { success: true, name: pattern.name };
123
- }
124
-
125
- /**
126
- * Trigger a file download in the browser.
127
- *
128
- * @param {string} filename
129
- * @param {string} content
130
- * @param {string} [mimeType='application/json']
131
- */
132
- export function downloadFile(filename, content, mimeType = 'application/json') {
133
- if (typeof document === 'undefined') {
134
- throw new Error('downloadFile requires a browser environment');
135
- }
136
-
137
- const blob = new Blob([content], { type: mimeType });
138
- const url = URL.createObjectURL(blob);
139
- const a = document.createElement('a');
140
- a.href = url;
141
- a.download = filename;
142
- a.style.display = 'none';
143
- // Prevent SPA router from intercepting the blob URL click
144
- a.addEventListener('click', (e) => e.stopPropagation());
145
- document.body.appendChild(a);
146
- a.click();
147
- document.body.removeChild(a);
148
- URL.revokeObjectURL(url);
149
- }