@adia-ai/a2ui-retrieval 0.0.1
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 +50 -0
- package/README.md +40 -0
- package/anti-patterns.js +148 -0
- package/catalog.js +215 -0
- package/clarity.js +207 -0
- package/component-entry.js +80 -0
- package/concept-mapper.js +127 -0
- package/context-assembler.js +168 -0
- package/decomposer.js +216 -0
- package/dialog-recorder.js +179 -0
- package/domain-router.js +172 -0
- package/embedding-provider.js +108 -0
- package/embedding-retriever.js +120 -0
- package/feedback-analyzer.js +235 -0
- package/feedback-store.js +175 -0
- package/feedback.js +198 -0
- package/gap-registry.js +121 -0
- package/index.js +16 -0
- package/intent-alignment.js +243 -0
- package/intent-categorizer.js +97 -0
- package/intent-gate.js +155 -0
- package/package.json +29 -0
- package/pattern-library.js +659 -0
- package/pattern-promotion.js +135 -0
- package/prompt-analyzer.js +211 -0
- package/synthetic-data.js +446 -0
- package/web-research.js +186 -0
- package/wiring-catalog.js +195 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SyntheticDataGenerator — Fills coverage gaps in training data by
|
|
3
|
+
* generating A2UI JSON examples for uncovered patterns.
|
|
4
|
+
*
|
|
5
|
+
* Uses the LLM adapter to generate schemas, validates them via the
|
|
6
|
+
* generative validator, scores quality via anti-pattern checks,
|
|
7
|
+
* and stores results as training pairs (prompt -> schema).
|
|
8
|
+
*
|
|
9
|
+
* Spec: A003 section 6 — Synthetic Data Generation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getCatalog } from './catalog.js';
|
|
13
|
+
import { getAntiPatterns } from './anti-patterns.js';
|
|
14
|
+
import { getAllPatterns } from './pattern-library.js';
|
|
15
|
+
import { serializeEntry } from './component-entry.js';
|
|
16
|
+
|
|
17
|
+
// ── Coverage targets (spec section 6.2) ──
|
|
18
|
+
|
|
19
|
+
const COVERAGE_TARGETS = [
|
|
20
|
+
{ id: 'sidebar-main', description: 'Sidebar navigation with main content area', complexity: 'medium' },
|
|
21
|
+
{ id: 'modal-form', description: 'Modal dialog containing a form with validation', complexity: 'medium' },
|
|
22
|
+
{ id: 'drawer-nav', description: 'Drawer panel with navigation links', complexity: 'medium' },
|
|
23
|
+
{ id: 'toast-sequence', description: 'Sequence of toast notifications (success, error, info)', complexity: 'low' },
|
|
24
|
+
{ id: 'tabs-content', description: 'Tabbed interface with different content per tab', complexity: 'medium' },
|
|
25
|
+
{ id: 'accordion-faq', description: 'Accordion with FAQ-style question/answer pairs', complexity: 'low' },
|
|
26
|
+
{ id: 'wizard-steps', description: 'Multi-step wizard with progress indicator and form fields', complexity: 'high' },
|
|
27
|
+
{ id: 'data-grid', description: 'Data table with sort, filter, and pagination', complexity: 'high' },
|
|
28
|
+
{ id: 'card-grid', description: 'Grid of cards with different content types', complexity: 'medium' },
|
|
29
|
+
{ id: 'auth-form', description: 'Login form with email, password, and social buttons', complexity: 'medium' },
|
|
30
|
+
{ id: 'profile-card', description: 'User profile card with avatar, name, stats, and actions', complexity: 'medium' },
|
|
31
|
+
{ id: 'empty-state', description: 'Empty state with illustration, heading, and CTA button', complexity: 'low' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// ── Default generation options ──
|
|
35
|
+
|
|
36
|
+
const DEFAULT_OPTIONS = {
|
|
37
|
+
temperature: 0.7,
|
|
38
|
+
maxRetries: 2,
|
|
39
|
+
batchSize: 4,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* SyntheticDataGenerator — generates A2UI training examples for coverage gaps.
|
|
44
|
+
*/
|
|
45
|
+
export class SyntheticDataGenerator {
|
|
46
|
+
#llmAdapter;
|
|
47
|
+
#catalog;
|
|
48
|
+
#patternLibrary;
|
|
49
|
+
#validator;
|
|
50
|
+
#antiPatterns;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {object} deps
|
|
54
|
+
* @param {object} deps.llmAdapter — LLM adapter with complete() method
|
|
55
|
+
* @param {object} [deps.catalog] — Component catalog (defaults to built-in)
|
|
56
|
+
* @param {object} [deps.patternLibrary] — Pattern library (defaults to built-in)
|
|
57
|
+
* @param {object} [deps.validator] — Schema validator with validateSchema()
|
|
58
|
+
* @param {object} [deps.antiPatterns] — Anti-patterns checker
|
|
59
|
+
*/
|
|
60
|
+
constructor({ llmAdapter, catalog, patternLibrary, validator, antiPatterns }) {
|
|
61
|
+
this.#llmAdapter = llmAdapter;
|
|
62
|
+
this.#catalog = catalog || null;
|
|
63
|
+
this.#patternLibrary = patternLibrary || null;
|
|
64
|
+
this.#validator = validator || null;
|
|
65
|
+
this.#antiPatterns = antiPatterns || null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Analyze coverage gaps — which target patterns are missing from existing examples.
|
|
70
|
+
*
|
|
71
|
+
* @param {object[]} existingExamples — Array of { name, template } pattern objects
|
|
72
|
+
* @returns {{ covered: string[], missing: string[], coverage: number }}
|
|
73
|
+
*/
|
|
74
|
+
analyzeCoverage(existingExamples) {
|
|
75
|
+
const existingNames = new Set(
|
|
76
|
+
(existingExamples || getAllPatterns()).map(p => p.name)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const covered = [];
|
|
80
|
+
const missing = [];
|
|
81
|
+
|
|
82
|
+
for (const target of COVERAGE_TARGETS) {
|
|
83
|
+
if (existingNames.has(target.id)) {
|
|
84
|
+
covered.push(target.id);
|
|
85
|
+
} else {
|
|
86
|
+
missing.push(target.id);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const total = COVERAGE_TARGETS.length;
|
|
91
|
+
const coverage = total === 0 ? 1 : covered.length / total;
|
|
92
|
+
|
|
93
|
+
return { covered, missing, coverage };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Generate synthetic examples for missing patterns.
|
|
98
|
+
*
|
|
99
|
+
* For each gap: builds a prompt, calls the LLM, validates, scores, and stores.
|
|
100
|
+
*
|
|
101
|
+
* @param {string[]} gaps — Pattern IDs to generate (from analyzeCoverage().missing)
|
|
102
|
+
* @param {object} [options]
|
|
103
|
+
* @param {string} [options.model] — Model override for the LLM adapter
|
|
104
|
+
* @param {number} [options.temperature] — Sampling temperature (default 0.7)
|
|
105
|
+
* @param {number} [options.maxRetries] — Max retries per pattern (default 2)
|
|
106
|
+
* @param {number} [options.batchSize] — Concurrent generation batch size (default 4)
|
|
107
|
+
* @returns {Promise<{ generated: object[], failed: string[], stats: object }>}
|
|
108
|
+
*/
|
|
109
|
+
async generateExamples(gaps, options = {}) {
|
|
110
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
111
|
+
const generated = [];
|
|
112
|
+
const failed = [];
|
|
113
|
+
let totalTokens = 0;
|
|
114
|
+
let totalAttempts = 0;
|
|
115
|
+
|
|
116
|
+
// Process in batches
|
|
117
|
+
for (let i = 0; i < gaps.length; i += opts.batchSize) {
|
|
118
|
+
const batch = gaps.slice(i, i + opts.batchSize);
|
|
119
|
+
|
|
120
|
+
const results = await Promise.allSettled(
|
|
121
|
+
batch.map(gapId => this.#generateWithRetry(gapId, opts))
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
for (let j = 0; j < results.length; j++) {
|
|
125
|
+
const result = results[j];
|
|
126
|
+
const gapId = batch[j];
|
|
127
|
+
|
|
128
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
129
|
+
generated.push(result.value);
|
|
130
|
+
totalTokens += result.value.tokenUsage || 0;
|
|
131
|
+
totalAttempts += result.value.attempts || 1;
|
|
132
|
+
} else {
|
|
133
|
+
failed.push(gapId);
|
|
134
|
+
totalAttempts += opts.maxRetries + 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
generated,
|
|
141
|
+
failed,
|
|
142
|
+
stats: {
|
|
143
|
+
total: gaps.length,
|
|
144
|
+
succeeded: generated.length,
|
|
145
|
+
failed: failed.length,
|
|
146
|
+
totalTokens,
|
|
147
|
+
totalAttempts,
|
|
148
|
+
averageQuality: generated.length > 0
|
|
149
|
+
? generated.reduce((sum, g) => sum + g.quality.overall, 0) / generated.length
|
|
150
|
+
: 0,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Generate a single training pair for a pattern description.
|
|
157
|
+
*
|
|
158
|
+
* @param {string} patternDescription — Natural language description of the pattern
|
|
159
|
+
* @returns {Promise<{ prompt: string, schema: object[], quality: object }>}
|
|
160
|
+
*/
|
|
161
|
+
async generateOne(patternDescription) {
|
|
162
|
+
const systemPrompt = await this.#buildSystemPrompt();
|
|
163
|
+
const userPrompt = this.#buildUserPrompt(patternDescription);
|
|
164
|
+
|
|
165
|
+
const response = await this.#llmAdapter.complete({
|
|
166
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
167
|
+
systemPrompt,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const schema = this.#parseResponse(response.content);
|
|
171
|
+
const quality = this.scoreExample(schema);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
prompt: patternDescription,
|
|
175
|
+
schema,
|
|
176
|
+
quality,
|
|
177
|
+
tokenUsage: (response.usage?.inputTokens || 0) + (response.usage?.outputTokens || 0),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Score a generated example against quality criteria.
|
|
183
|
+
*
|
|
184
|
+
* Uses the anti-patterns checker from the intelligence system to detect
|
|
185
|
+
* structural issues, missing props, anti-patterns, and unnecessary wrappers.
|
|
186
|
+
*
|
|
187
|
+
* @param {object[]} schema — A2UI messages array
|
|
188
|
+
* @returns {{ structural: number, completeness: number, idiomatic: number, minimal: number, overall: number }}
|
|
189
|
+
*/
|
|
190
|
+
scoreExample(schema) {
|
|
191
|
+
// Collect all components across messages
|
|
192
|
+
const allComponents = [];
|
|
193
|
+
for (const msg of schema) {
|
|
194
|
+
if (msg.type === 'updateComponents' && Array.isArray(msg.components)) {
|
|
195
|
+
allComponents.push(...msg.components);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Run validation if available
|
|
200
|
+
let validationIssues = [];
|
|
201
|
+
if (this.#validator) {
|
|
202
|
+
const validation = this.#validator(schema);
|
|
203
|
+
validationIssues = (validation.checks || []).filter(c => !c.passed);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Run anti-pattern checks (HTML-based)
|
|
207
|
+
// Serialize components to a minimal HTML representation for pattern matching
|
|
208
|
+
const html = this.#componentsToHtml(allComponents);
|
|
209
|
+
const antiPatternChecks = this.#antiPatterns
|
|
210
|
+
? this.#antiPatterns(html)
|
|
211
|
+
: [];
|
|
212
|
+
|
|
213
|
+
// Structural: no orphaned children, valid message format, root exists
|
|
214
|
+
const structuralIssues = validationIssues.filter(c =>
|
|
215
|
+
['hasRootComponent', 'noOrphanedChildren', 'validMessageFormat', 'flatAdjacency'].includes(c.name)
|
|
216
|
+
);
|
|
217
|
+
const structural = structuralIssues.length === 0 ? 1 : 0.5;
|
|
218
|
+
|
|
219
|
+
// Completeness: text content set, all types registered
|
|
220
|
+
const completenessIssues = validationIssues.filter(c =>
|
|
221
|
+
['textContentSet', 'allTypesRegistered'].includes(c.name)
|
|
222
|
+
);
|
|
223
|
+
const completeness = Math.max(0, 1 - (completenessIssues.length * 0.1));
|
|
224
|
+
|
|
225
|
+
// Idiomatic: no anti-patterns (bare divs, inline styles, wrong nesting)
|
|
226
|
+
const idiomaticViolations = antiPatternChecks.filter(ap =>
|
|
227
|
+
['noBareDivs', 'noBareInputs', 'cardStructure', 'noInventedComponents'].includes(ap.name)
|
|
228
|
+
);
|
|
229
|
+
const idiomatic = idiomaticViolations.length === 0 ? 1 : 0.5;
|
|
230
|
+
|
|
231
|
+
// Minimal: no unnecessary wrappers or inline layout/colors
|
|
232
|
+
const minimalViolations = antiPatternChecks.filter(ap =>
|
|
233
|
+
['noHardcodedColors', 'noInlineLayout'].includes(ap.name)
|
|
234
|
+
);
|
|
235
|
+
const minimal = minimalViolations.length === 0 ? 1 : 0.5;
|
|
236
|
+
|
|
237
|
+
// Overall weighted average
|
|
238
|
+
const overall = (structural * 0.3) + (completeness * 0.25) + (idiomatic * 0.25) + (minimal * 0.2);
|
|
239
|
+
|
|
240
|
+
return { structural, completeness, idiomatic, minimal, overall };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── Private helpers ──
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Generate with retry logic.
|
|
247
|
+
* @param {string} gapId — Target pattern ID
|
|
248
|
+
* @param {object} opts — Generation options
|
|
249
|
+
* @returns {Promise<object|null>}
|
|
250
|
+
*/
|
|
251
|
+
async #generateWithRetry(gapId, opts) {
|
|
252
|
+
const target = COVERAGE_TARGETS.find(t => t.id === gapId);
|
|
253
|
+
if (!target) return null;
|
|
254
|
+
|
|
255
|
+
let lastError;
|
|
256
|
+
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
|
|
257
|
+
try {
|
|
258
|
+
const result = await this.generateOne(target.description);
|
|
259
|
+
|
|
260
|
+
// Require minimum quality threshold
|
|
261
|
+
if (result.quality.overall >= 0.5) {
|
|
262
|
+
return {
|
|
263
|
+
id: gapId,
|
|
264
|
+
...result,
|
|
265
|
+
attempts: attempt + 1,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
lastError = new Error(`Quality too low: ${result.quality.overall}`);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
lastError = err;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
throw lastError;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Build the system prompt with catalog, anti-patterns, and format rules.
|
|
280
|
+
* @returns {string}
|
|
281
|
+
*/
|
|
282
|
+
async #buildSystemPrompt() {
|
|
283
|
+
const parts = [];
|
|
284
|
+
|
|
285
|
+
// Role
|
|
286
|
+
parts.push('You are an A2UI training data generator for the AdiaUI design system. Output ONLY a JSON array of A2UI messages.');
|
|
287
|
+
|
|
288
|
+
// Output format
|
|
289
|
+
parts.push('Output format: [{ "type": "updateComponents", "surfaceId": "default", "components": [...] }]');
|
|
290
|
+
parts.push('Components use flat adjacency: each has { id, component, children?: [string ids], ...props }.');
|
|
291
|
+
parts.push('The root component must have id "root".');
|
|
292
|
+
|
|
293
|
+
// Anti-patterns (rules)
|
|
294
|
+
const antiPatterns = getAntiPatterns();
|
|
295
|
+
if (antiPatterns.length > 0) {
|
|
296
|
+
const rules = antiPatterns.map(ap => `- ${ap.description}`).join('\n');
|
|
297
|
+
parts.push(`Rules:\n${rules}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Available components (summary level)
|
|
301
|
+
const catalog = this.#catalog || await getCatalog();
|
|
302
|
+
const entries = catalog.entries || new Map();
|
|
303
|
+
if (entries.size > 0) {
|
|
304
|
+
const lines = [];
|
|
305
|
+
for (const entry of entries.values()) {
|
|
306
|
+
const serialized = serializeEntry(entry, 'index');
|
|
307
|
+
lines.push(`- ${serialized.type} (${serialized.tag}): ${serialized.description || ''}`);
|
|
308
|
+
}
|
|
309
|
+
parts.push(`Available components:\n${lines.join('\n')}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Quality criteria
|
|
313
|
+
parts.push([
|
|
314
|
+
'Quality criteria:',
|
|
315
|
+
'- Structural: valid root, all children resolve, flat adjacency list',
|
|
316
|
+
'- Completeness: all Text components have textContent, all types are registered',
|
|
317
|
+
'- Idiomatic: use Card > Header + Section + Footer, no bare divs, no invented components',
|
|
318
|
+
'- Minimal: no inline styles, no hardcoded colors, use semantic props and variants',
|
|
319
|
+
].join('\n'));
|
|
320
|
+
|
|
321
|
+
return parts.join('\n\n');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Build the user prompt for a specific pattern.
|
|
326
|
+
* @param {string} patternDescription
|
|
327
|
+
* @returns {string}
|
|
328
|
+
*/
|
|
329
|
+
#buildUserPrompt(patternDescription) {
|
|
330
|
+
return [
|
|
331
|
+
`Generate an A2UI component tree for this UI pattern:`,
|
|
332
|
+
``,
|
|
333
|
+
`Pattern: ${patternDescription}`,
|
|
334
|
+
``,
|
|
335
|
+
`Requirements:`,
|
|
336
|
+
`- Use realistic content (names, values, labels)`,
|
|
337
|
+
`- Follow Card > Header + Section + Footer anatomy where appropriate`,
|
|
338
|
+
`- Use layout components (Row, Column, Grid) for composition`,
|
|
339
|
+
`- Include all necessary props (text, variant, label, placeholder, etc.)`,
|
|
340
|
+
`- Output valid JSON — no markdown, no explanation`,
|
|
341
|
+
].join('\n');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Parse an LLM response into A2UI messages.
|
|
346
|
+
* Handles raw JSON, markdown code fences, bare component arrays.
|
|
347
|
+
*
|
|
348
|
+
* @param {string} content — Raw LLM response
|
|
349
|
+
* @returns {object[]}
|
|
350
|
+
*/
|
|
351
|
+
#parseResponse(content) {
|
|
352
|
+
if (!content || typeof content !== 'string') {
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
let json = content.trim();
|
|
357
|
+
|
|
358
|
+
// Strip markdown code fences
|
|
359
|
+
const fenceMatch = json.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
|
|
360
|
+
if (fenceMatch) {
|
|
361
|
+
json = fenceMatch[1].trim();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const parsed = JSON.parse(json);
|
|
366
|
+
|
|
367
|
+
// Array of messages
|
|
368
|
+
if (Array.isArray(parsed)) {
|
|
369
|
+
if (parsed.length > 0 && parsed[0].type === 'updateComponents') {
|
|
370
|
+
return parsed;
|
|
371
|
+
}
|
|
372
|
+
// Bare components array — wrap
|
|
373
|
+
if (parsed.length > 0 && parsed[0].id && parsed[0].component) {
|
|
374
|
+
return [{ type: 'updateComponents', surfaceId: 'default', components: parsed }];
|
|
375
|
+
}
|
|
376
|
+
return parsed;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Single message object
|
|
380
|
+
if (parsed && typeof parsed === 'object' && parsed.type === 'updateComponents') {
|
|
381
|
+
return [parsed];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Single component — wrap
|
|
385
|
+
if (parsed && typeof parsed === 'object' && parsed.id && parsed.component) {
|
|
386
|
+
return [{ type: 'updateComponents', surfaceId: 'default', components: [parsed] }];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return [];
|
|
390
|
+
} catch {
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Convert a flat component list to a minimal HTML-like string for anti-pattern checking.
|
|
397
|
+
* The anti-patterns module uses regex/function checks on HTML strings.
|
|
398
|
+
*
|
|
399
|
+
* @param {object[]} components
|
|
400
|
+
* @returns {string}
|
|
401
|
+
*/
|
|
402
|
+
#componentsToHtml(components) {
|
|
403
|
+
const byId = new Map(components.map(c => [c.id, c]));
|
|
404
|
+
const lines = [];
|
|
405
|
+
|
|
406
|
+
for (const comp of components) {
|
|
407
|
+
const type = comp.component;
|
|
408
|
+
if (!type) continue;
|
|
409
|
+
|
|
410
|
+
// Map A2UI types to their AdiaUI tag names for anti-pattern checking
|
|
411
|
+
const tag = type.toLowerCase().replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
412
|
+
const tagName = tag.endsWith('-n') ? tag : `${tag}-n`;
|
|
413
|
+
|
|
414
|
+
// Build a minimal HTML representation
|
|
415
|
+
const attrs = [];
|
|
416
|
+
if (comp.style) {
|
|
417
|
+
attrs.push(`style="${typeof comp.style === 'object' ? Object.entries(comp.style).map(([k, v]) => `${k}:${v}`).join(';') : comp.style}"`);
|
|
418
|
+
}
|
|
419
|
+
const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
|
|
420
|
+
|
|
421
|
+
// Represent nesting for structural checks
|
|
422
|
+
if (Array.isArray(comp.children)) {
|
|
423
|
+
const childTypes = comp.children
|
|
424
|
+
.map(id => byId.get(id))
|
|
425
|
+
.filter(Boolean)
|
|
426
|
+
.map(c => {
|
|
427
|
+
const ct = c.component?.toLowerCase().replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() || '';
|
|
428
|
+
return ct.endsWith('-n') ? ct : ct;
|
|
429
|
+
});
|
|
430
|
+
lines.push(`<${tagName}${attrStr}>${childTypes.map(ct => `<${ct}>`).join('')}</${tagName}>`);
|
|
431
|
+
} else {
|
|
432
|
+
lines.push(`<${tagName}${attrStr}></${tagName}>`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return lines.join('\n');
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Get the list of coverage targets.
|
|
442
|
+
* @returns {Array<{ id: string, description: string, complexity: string }>}
|
|
443
|
+
*/
|
|
444
|
+
export function getCoverageTargets() {
|
|
445
|
+
return [...COVERAGE_TARGETS];
|
|
446
|
+
}
|
package/web-research.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Research — Enriches generation context with web search results.
|
|
3
|
+
*
|
|
4
|
+
* Detects reference mentions in intents ("like Stripe", "Notion-style"),
|
|
5
|
+
* generates targeted search queries, fetches results, and extracts
|
|
6
|
+
* UI-relevant patterns to feed into the generation prompt.
|
|
7
|
+
*
|
|
8
|
+
* Works with any search function that matches:
|
|
9
|
+
* search(query) → Promise<{ results: { title, snippet, url }[] }>
|
|
10
|
+
*
|
|
11
|
+
* Falls back to LLM knowledge when no search function is provided.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ── Reference detection ──────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/** Patterns that indicate the user is referencing a specific product/design */
|
|
17
|
+
const REFERENCE_PATTERNS = [
|
|
18
|
+
/\blike\s+(\w[\w\s]*?)(?:\s*['']s|\s+style|\s+design|\s*$)/i,
|
|
19
|
+
/\b(\w[\w\s]*?)[\s-]style\b/i,
|
|
20
|
+
/\b(\w[\w\s]*?)[\s-]inspired\b/i,
|
|
21
|
+
/\bsimilar\s+to\s+(\w[\w\s]*?)(?:\s|$|,)/i,
|
|
22
|
+
/\bbased\s+on\s+(\w[\w\s]*?)(?:\s|$|,)/i,
|
|
23
|
+
/\bcopy\s+(\w[\w\s]*?)(?:\s|$|,)/i,
|
|
24
|
+
/\bclone\s+(\w[\w\s]*?)(?:\s|$|,)/i,
|
|
25
|
+
/\b(\w+)\s+(?:pricing|dashboard|landing|login|settings|profile)\s+page\b/i,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/** Well-known product/brand names for UI reference */
|
|
29
|
+
const KNOWN_REFERENCES = new Set([
|
|
30
|
+
'stripe', 'notion', 'linear', 'figma', 'slack', 'discord', 'spotify',
|
|
31
|
+
'github', 'vercel', 'netlify', 'supabase', 'firebase', 'tailwind',
|
|
32
|
+
'shadcn', 'radix', 'chakra', 'material', 'ant', 'bootstrap',
|
|
33
|
+
'shopify', 'airbnb', 'uber', 'twitter', 'instagram', 'dribbble',
|
|
34
|
+
'apple', 'google', 'microsoft', 'amazon', 'netflix',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Detect product/brand references in an intent.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} intent
|
|
41
|
+
* @returns {{ references: string[], queries: string[] }}
|
|
42
|
+
*/
|
|
43
|
+
export function detectReferences(intent) {
|
|
44
|
+
const references = [];
|
|
45
|
+
const lower = intent.toLowerCase();
|
|
46
|
+
|
|
47
|
+
// Pattern matching
|
|
48
|
+
for (const pattern of REFERENCE_PATTERNS) {
|
|
49
|
+
const match = intent.match(pattern);
|
|
50
|
+
if (match) {
|
|
51
|
+
let ref = (match[1] || '').trim();
|
|
52
|
+
// Strip leading articles
|
|
53
|
+
ref = ref.replace(/^(the|a|an)\s+/i, '');
|
|
54
|
+
if (ref && ref.length > 1 && ref.length < 30) {
|
|
55
|
+
references.push(ref);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Direct brand name detection (only as whole words)
|
|
61
|
+
for (const brand of KNOWN_REFERENCES) {
|
|
62
|
+
const re = new RegExp(`\\b${brand}\\b`, 'i');
|
|
63
|
+
if (re.test(lower) && !references.some(r => r.toLowerCase() === brand)) {
|
|
64
|
+
references.push(brand);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Filter out non-brand captures (e.g., "inspired" from "X-inspired")
|
|
69
|
+
const NON_BRANDS = new Set(['inspired', 'style', 'based', 'like', 'similar', 'the']);
|
|
70
|
+
for (let i = references.length - 1; i >= 0; i--) {
|
|
71
|
+
if (NON_BRANDS.has(references[i].toLowerCase())) references.splice(i, 1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Generate search queries from references + intent context
|
|
75
|
+
const queries = [];
|
|
76
|
+
const uniqueRefs = [...new Set(references.map(r => r.toLowerCase()))];
|
|
77
|
+
|
|
78
|
+
for (const ref of uniqueRefs.slice(0, 2)) {
|
|
79
|
+
// Extract the UI type from the intent
|
|
80
|
+
const uiType = extractUIType(intent);
|
|
81
|
+
if (uiType) {
|
|
82
|
+
queries.push(`${ref} ${uiType} UI design components`);
|
|
83
|
+
} else {
|
|
84
|
+
queries.push(`${ref} UI design pattern layout`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// If no references but intent mentions specific UI patterns
|
|
89
|
+
if (queries.length === 0) {
|
|
90
|
+
const uiType = extractUIType(intent);
|
|
91
|
+
if (uiType && intent.split(/\s+/).length < 6) {
|
|
92
|
+
// Short intent about a UI type — search for best practices
|
|
93
|
+
queries.push(`best ${uiType} UI design patterns 2024`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { references: uniqueRefs, queries };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Extract the UI type/pattern from an intent (pricing, dashboard, login, etc.)
|
|
102
|
+
*/
|
|
103
|
+
function extractUIType(intent) {
|
|
104
|
+
const lower = intent.toLowerCase();
|
|
105
|
+
const types = [
|
|
106
|
+
'pricing', 'dashboard', 'landing page', 'login', 'signup', 'settings',
|
|
107
|
+
'profile', 'onboarding', 'checkout', 'notification', 'kanban', 'calendar',
|
|
108
|
+
'chat', 'sidebar', 'navbar', 'hero', 'footer', 'table', 'form',
|
|
109
|
+
'modal', 'card', 'timeline', 'analytics', 'admin',
|
|
110
|
+
];
|
|
111
|
+
return types.find(t => lower.includes(t)) || null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Research UI patterns for an intent using web search.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} intent — User's generation intent
|
|
118
|
+
* @param {object} [options]
|
|
119
|
+
* @param {(query: string) => Promise<{ results: { title: string, snippet: string, url: string }[] }>} [options.search] — Search function
|
|
120
|
+
* @param {object} [options.llmAdapter] — LLM adapter for summarization
|
|
121
|
+
* @returns {Promise<{ references: string[], insights: string[], searchResults: object[], context: string }>}
|
|
122
|
+
*/
|
|
123
|
+
export async function researchIntent(intent, options = {}) {
|
|
124
|
+
const { search, llmAdapter } = options;
|
|
125
|
+
const { references, queries } = detectReferences(intent);
|
|
126
|
+
|
|
127
|
+
// No references and no search → return empty
|
|
128
|
+
if (references.length === 0 && queries.length === 0) {
|
|
129
|
+
return { references: [], insights: [], searchResults: [], context: '' };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Fetch search results ──
|
|
133
|
+
const searchResults = [];
|
|
134
|
+
if (search && queries.length > 0) {
|
|
135
|
+
for (const query of queries.slice(0, 2)) {
|
|
136
|
+
try {
|
|
137
|
+
const result = await search(query);
|
|
138
|
+
if (result?.results?.length) {
|
|
139
|
+
searchResults.push(...result.results.slice(0, 3));
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// Search failed — continue without it
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Build context from results ──
|
|
148
|
+
const insights = [];
|
|
149
|
+
|
|
150
|
+
if (searchResults.length > 0) {
|
|
151
|
+
// Extract UI-relevant snippets
|
|
152
|
+
for (const result of searchResults) {
|
|
153
|
+
if (result.snippet) {
|
|
154
|
+
insights.push(`${result.title}: ${result.snippet}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── LLM-powered reference description (when search has no results) ──
|
|
160
|
+
if (insights.length === 0 && llmAdapter && references.length > 0) {
|
|
161
|
+
try {
|
|
162
|
+
const refList = references.join(', ');
|
|
163
|
+
const uiType = extractUIType(intent) || 'UI';
|
|
164
|
+
const response = await llmAdapter.complete({
|
|
165
|
+
messages: [{ role: 'user', content: `Briefly describe the ${uiType} design of ${refList}. Focus on: layout structure, key components, visual hierarchy, and interaction patterns. 3-4 sentences max.` }],
|
|
166
|
+
systemPrompt: 'You are a UI design expert. Give concise, factual descriptions of well-known product UIs. Focus on structure and components, not opinions.',
|
|
167
|
+
});
|
|
168
|
+
if (response.content) {
|
|
169
|
+
insights.push(response.content);
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
// LLM failed — continue without it
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Compile context string for the generation prompt ──
|
|
177
|
+
let context = '';
|
|
178
|
+
if (references.length > 0) {
|
|
179
|
+
context += `Reference designs: ${references.join(', ')}\n`;
|
|
180
|
+
}
|
|
181
|
+
if (insights.length > 0) {
|
|
182
|
+
context += `Design insights:\n${insights.map(i => `- ${i}`).join('\n')}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { references, insights, searchResults, context };
|
|
186
|
+
}
|