@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,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ComponentEntry — Detail level serialization for catalog entries.
|
|
3
|
+
*
|
|
4
|
+
* Three progressive-disclosure levels:
|
|
5
|
+
* Index (~10 tokens): { type, tag, category }
|
|
6
|
+
* Summary (~50 tokens): index + { description, properties: [names] }
|
|
7
|
+
* Reference (~200 tokens): summary + { properties: [full], slots, events, children, tokens }
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** @typedef {'index' | 'summary' | 'reference'} DetailLevel */
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Serialize a component entry to the requested detail level.
|
|
14
|
+
*
|
|
15
|
+
* @param {object} entry — Full component entry from the catalog
|
|
16
|
+
* @param {DetailLevel} level — Detail level
|
|
17
|
+
* @returns {object} — Serialized entry at the requested level
|
|
18
|
+
*/
|
|
19
|
+
export function serializeEntry(entry, level = 'index') {
|
|
20
|
+
if (level === 'index') return serializeIndex(entry);
|
|
21
|
+
if (level === 'summary') return serializeSummary(entry);
|
|
22
|
+
return serializeReference(entry);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function serializeIndex(entry) {
|
|
26
|
+
return {
|
|
27
|
+
type: entry.type,
|
|
28
|
+
tag: entry.tag,
|
|
29
|
+
category: entry.category,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function serializeSummary(entry) {
|
|
34
|
+
const index = serializeIndex(entry);
|
|
35
|
+
const result = {
|
|
36
|
+
...index,
|
|
37
|
+
description: entry.description || '',
|
|
38
|
+
};
|
|
39
|
+
if (entry.schema?.properties) {
|
|
40
|
+
result.properties = Object.keys(entry.schema.properties);
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function serializeReference(entry) {
|
|
46
|
+
const summary = serializeSummary(entry);
|
|
47
|
+
const schema = entry.schema;
|
|
48
|
+
if (!schema) return summary;
|
|
49
|
+
|
|
50
|
+
const result = { ...summary };
|
|
51
|
+
|
|
52
|
+
// Full property definitions
|
|
53
|
+
if (schema.properties) {
|
|
54
|
+
result.properties = Object.entries(schema.properties).map(([name, def]) => ({
|
|
55
|
+
name,
|
|
56
|
+
type: def.type,
|
|
57
|
+
...(def.enum ? { enum: def.enum } : {}),
|
|
58
|
+
...(def.default !== undefined ? { default: def.default } : {}),
|
|
59
|
+
...(def.description ? { description: def.description } : {}),
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (schema.slots && Object.keys(schema.slots).length > 0) {
|
|
64
|
+
result.slots = schema.slots;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (schema.events && Object.keys(schema.events).length > 0) {
|
|
68
|
+
result.events = schema.events;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (schema.children) {
|
|
72
|
+
result.children = schema.children;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (schema.tokens) {
|
|
76
|
+
result.tokens = schema.tokens;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concept Mapper — re-rank corpus patterns using analyzer signals.
|
|
3
|
+
*
|
|
4
|
+
* The existing `searchBlocks(query)` does pure keyword matching against
|
|
5
|
+
* pattern.keywords + pattern.description. That's necessary but not sufficient
|
|
6
|
+
* — a prompt about a "user dashboard" might lexically prefer a pattern named
|
|
7
|
+
* "user-profile" over "admin-dashboard" even though the latter is a better
|
|
8
|
+
* conceptual match.
|
|
9
|
+
*
|
|
10
|
+
* This module takes the structured analysis produced by prompt-analyzer.js
|
|
11
|
+
* and combines THREE signals into a final pattern score:
|
|
12
|
+
*
|
|
13
|
+
* lexical: the original keyword score from searchBlocks (steelman → keywords)
|
|
14
|
+
* conceptual: overlap between analysis.concepts and pattern.tags.purpose
|
|
15
|
+
* structural: overlap between analysis.impliedComponents and the set of
|
|
16
|
+
* component types referenced in the pattern's template
|
|
17
|
+
*
|
|
18
|
+
* Patterns missing tags fall back to lexical-only scoring (graceful
|
|
19
|
+
* degradation; the older patterns in the corpus haven't all been retagged).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { searchBlocks } from '../engine/reference.js';
|
|
23
|
+
import { scoreAll as embeddingScoreAll, available as embeddingAvailable } from './embedding-retriever.js';
|
|
24
|
+
|
|
25
|
+
/** Weights for the combined score. Tuned to keep lexical authoritative
|
|
26
|
+
* but let strong conceptual+structural+semantic signals override marginal
|
|
27
|
+
* lexical differences. Embedding semantic similarity scores are in [-1, 1]
|
|
28
|
+
* so their weight is high (30) — cosine of 0.8 → 24 points, comparable to
|
|
29
|
+
* 3 concept-tag hits. Tuned to fix the "product card → ticket form"
|
|
30
|
+
* keyword-collision class of failure. */
|
|
31
|
+
const WEIGHTS = {
|
|
32
|
+
lexical: 1.0, // baseline
|
|
33
|
+
conceptual: 8, // each matching concept-tag adds 8 points
|
|
34
|
+
structural: 1.5, // each matching component-signature element adds 1.5 points
|
|
35
|
+
semantic: 30, // cosine(query_embedding, pattern_embedding) × 30
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Re-rank pattern matches using the analyzer's structured signals.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} opts
|
|
42
|
+
* @param {object} opts.analysis — Output of analyzePrompt() from prompt-analyzer.js
|
|
43
|
+
* @param {string} [opts.domain] — Domain hint (passed through to searchBlocks)
|
|
44
|
+
* @param {number} [opts.limit=10] — Cap on returned results
|
|
45
|
+
* @returns {Array<{ pattern: object, score: number, breakdown: object }>}
|
|
46
|
+
* Patterns ranked by combined score, descending.
|
|
47
|
+
*/
|
|
48
|
+
export async function rankByConceptAndSignature({ analysis, domain, limit = 10 }) {
|
|
49
|
+
if (!analysis) return [];
|
|
50
|
+
|
|
51
|
+
// Use the steelmanned brief for lexical search — it's denser than the raw
|
|
52
|
+
// intent and surfaces keywords the user implied but didn't say.
|
|
53
|
+
const query = analysis.steelman || analysis.raw;
|
|
54
|
+
const lexicalHits = searchBlocks(query, { domain });
|
|
55
|
+
if (!Array.isArray(lexicalHits) || lexicalHits.length === 0) return [];
|
|
56
|
+
|
|
57
|
+
const conceptSet = new Set((analysis.concepts || []).map(c => c.toLowerCase()));
|
|
58
|
+
const componentSet = new Set(analysis.impliedComponents || []);
|
|
59
|
+
|
|
60
|
+
// Semantic channel — only run when the index + provider are both available.
|
|
61
|
+
// Returns a Map<patternName, cosineSim>. Silent no-op otherwise.
|
|
62
|
+
const semanticMap = (await embeddingAvailable())
|
|
63
|
+
? await embeddingScoreAll(query)
|
|
64
|
+
: new Map();
|
|
65
|
+
|
|
66
|
+
const ranked = lexicalHits.map(hit => {
|
|
67
|
+
// Different shapes of hit objects in the corpus — be permissive.
|
|
68
|
+
const pattern = hit.pattern || hit;
|
|
69
|
+
const lexicalScore = hit.score ?? hit.confidence ?? 1;
|
|
70
|
+
|
|
71
|
+
const conceptScore = scoreConceptOverlap(pattern, conceptSet);
|
|
72
|
+
const structuralScore = scoreSignatureOverlap(pattern, componentSet);
|
|
73
|
+
// Clamp negative cosines to 0 — no retrieval value in "most anti-similar".
|
|
74
|
+
const semanticScore = Math.max(0, semanticMap.get(pattern.name) || 0);
|
|
75
|
+
|
|
76
|
+
const combined =
|
|
77
|
+
WEIGHTS.lexical * lexicalScore +
|
|
78
|
+
WEIGHTS.conceptual * conceptScore +
|
|
79
|
+
WEIGHTS.structural * structuralScore +
|
|
80
|
+
WEIGHTS.semantic * semanticScore;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
pattern,
|
|
84
|
+
score: combined,
|
|
85
|
+
breakdown: {
|
|
86
|
+
lexical: +(WEIGHTS.lexical * lexicalScore).toFixed(2),
|
|
87
|
+
conceptual: +(WEIGHTS.conceptual * conceptScore).toFixed(2),
|
|
88
|
+
structural: +(WEIGHTS.structural * structuralScore).toFixed(2),
|
|
89
|
+
semantic: +(WEIGHTS.semantic * semanticScore).toFixed(2),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
ranked.sort((a, b) => b.score - a.score);
|
|
95
|
+
return ranked.slice(0, limit);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Count how many of the analysis concepts appear in the pattern's tag system. */
|
|
99
|
+
function scoreConceptOverlap(pattern, conceptSet) {
|
|
100
|
+
if (!pattern || conceptSet.size === 0) return 0;
|
|
101
|
+
const tags = pattern.tags || {};
|
|
102
|
+
const flat = []
|
|
103
|
+
.concat(tags.purpose || [])
|
|
104
|
+
.concat(tags.layout || [])
|
|
105
|
+
.concat(tags.interaction || [])
|
|
106
|
+
.concat(pattern.keywords || [])
|
|
107
|
+
.map(t => String(t).toLowerCase());
|
|
108
|
+
let hits = 0;
|
|
109
|
+
for (const c of conceptSet) if (flat.includes(c)) hits++;
|
|
110
|
+
return hits;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Count how many of the implied components appear in the pattern's template. */
|
|
114
|
+
function scoreSignatureOverlap(pattern, componentSet) {
|
|
115
|
+
if (!pattern || componentSet.size === 0) return 0;
|
|
116
|
+
const template = pattern.template;
|
|
117
|
+
if (!Array.isArray(template)) return 0;
|
|
118
|
+
// Build the pattern's component signature once per call; small templates
|
|
119
|
+
// mean the cost is negligible. Cache later if hot.
|
|
120
|
+
const sig = new Set();
|
|
121
|
+
for (const node of template) {
|
|
122
|
+
if (node && typeof node.component === 'string') sig.add(node.component);
|
|
123
|
+
}
|
|
124
|
+
let hits = 0;
|
|
125
|
+
for (const c of componentSet) if (sig.has(c)) hits++;
|
|
126
|
+
return hits;
|
|
127
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Assembler — Progressive-disclosure context packaging with token budgets.
|
|
3
|
+
*
|
|
4
|
+
* Assembles context at 5 budget tiers, combining component entries,
|
|
5
|
+
* domain-relevant patterns, and anti-patterns within token limits.
|
|
6
|
+
*
|
|
7
|
+
* Token estimation: ~1 token per 4 characters of JSON output.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getCatalog, getComponentsByCategory } from './catalog.js';
|
|
11
|
+
import { serializeEntry } from './component-entry.js';
|
|
12
|
+
import { classifyIntent, getDomain } from './domain-router.js';
|
|
13
|
+
import { getAntiPatterns } from './anti-patterns.js';
|
|
14
|
+
import { searchPatterns, getAllPatterns } from './pattern-library.js';
|
|
15
|
+
|
|
16
|
+
/** Token budget per tier */
|
|
17
|
+
const TIER_BUDGETS = {
|
|
18
|
+
0: 500,
|
|
19
|
+
1: 2000,
|
|
20
|
+
2: 5000,
|
|
21
|
+
3: 10000,
|
|
22
|
+
4: 20000,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Estimate token count from a JavaScript value (serialized as JSON).
|
|
27
|
+
* @param {*} value
|
|
28
|
+
* @returns {number}
|
|
29
|
+
*/
|
|
30
|
+
function estimateTokens(value) {
|
|
31
|
+
const json = JSON.stringify(value);
|
|
32
|
+
return Math.ceil(json.length / 4);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Assemble context for a given intent at a specific budget tier.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} options
|
|
39
|
+
* @param {string} [options.intent] — Natural language intent (used for domain classification)
|
|
40
|
+
* @param {number} [options.tier=1] — Budget tier (0-4)
|
|
41
|
+
* @param {string} [options.domain] — Override domain (skips classification)
|
|
42
|
+
* @returns {{ components: object[], patterns: object[], antiPatterns: object[], domain: object, estimatedTokens: number }}
|
|
43
|
+
*/
|
|
44
|
+
export async function assembleContext({ intent = '', tier = 1, domain: overrideDomain } = {}) {
|
|
45
|
+
const budget = TIER_BUDGETS[tier] ?? TIER_BUDGETS[1];
|
|
46
|
+
|
|
47
|
+
// Classify domain
|
|
48
|
+
const classification = intent ? classifyIntent(intent) : null;
|
|
49
|
+
const domainName = overrideDomain || classification?.domain || 'layout';
|
|
50
|
+
const domainConfig = getDomain(domainName);
|
|
51
|
+
|
|
52
|
+
let usedTokens = 0;
|
|
53
|
+
const result = {
|
|
54
|
+
components: [],
|
|
55
|
+
patterns: [],
|
|
56
|
+
antiPatterns: [],
|
|
57
|
+
domain: {
|
|
58
|
+
name: domainName,
|
|
59
|
+
confidence: classification?.confidence ?? 1,
|
|
60
|
+
matchedSignals: classification?.matchedSignals ?? [],
|
|
61
|
+
},
|
|
62
|
+
estimatedTokens: 0,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Account for domain metadata
|
|
66
|
+
usedTokens += estimateTokens(result.domain);
|
|
67
|
+
|
|
68
|
+
const catalog = await getCatalog();
|
|
69
|
+
|
|
70
|
+
// ── Tier 0: Component index only ──
|
|
71
|
+
if (tier === 0) {
|
|
72
|
+
for (const entry of catalog.entries.values()) {
|
|
73
|
+
const serialized = serializeEntry(entry, 'index');
|
|
74
|
+
const cost = estimateTokens(serialized);
|
|
75
|
+
if (usedTokens + cost > budget) break;
|
|
76
|
+
result.components.push(serialized);
|
|
77
|
+
usedTokens += cost;
|
|
78
|
+
}
|
|
79
|
+
result.estimatedTokens = usedTokens;
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Tier 1+: Domain-relevant summaries first ──
|
|
84
|
+
const domainComponents = domainConfig?.components || [];
|
|
85
|
+
const addedTypes = new Set();
|
|
86
|
+
|
|
87
|
+
// Add domain-relevant components as summaries
|
|
88
|
+
for (const typeName of domainComponents) {
|
|
89
|
+
const entry = findEntry(catalog, typeName);
|
|
90
|
+
if (!entry) continue;
|
|
91
|
+
|
|
92
|
+
const level = tier >= 2 ? 'reference' : 'summary';
|
|
93
|
+
const serialized = serializeEntry(entry, level);
|
|
94
|
+
const cost = estimateTokens(serialized);
|
|
95
|
+
if (usedTokens + cost > budget) break;
|
|
96
|
+
|
|
97
|
+
result.components.push(serialized);
|
|
98
|
+
addedTypes.add(entry.type);
|
|
99
|
+
usedTokens += cost;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Tier 2+: Fill remaining budget with other components ──
|
|
103
|
+
if (tier >= 2) {
|
|
104
|
+
for (const entry of catalog.entries.values()) {
|
|
105
|
+
if (addedTypes.has(entry.type)) continue;
|
|
106
|
+
|
|
107
|
+
const level = tier >= 3 ? 'reference' : 'summary';
|
|
108
|
+
const serialized = serializeEntry(entry, level);
|
|
109
|
+
const cost = estimateTokens(serialized);
|
|
110
|
+
if (usedTokens + cost > budget) break;
|
|
111
|
+
|
|
112
|
+
result.components.push(serialized);
|
|
113
|
+
addedTypes.add(entry.type);
|
|
114
|
+
usedTokens += cost;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Tier 1+: Add anti-patterns ──
|
|
119
|
+
if (tier >= 1) {
|
|
120
|
+
const ap = getAntiPatterns();
|
|
121
|
+
const apCost = estimateTokens(ap);
|
|
122
|
+
if (usedTokens + apCost <= budget) {
|
|
123
|
+
result.antiPatterns = ap;
|
|
124
|
+
usedTokens += apCost;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Tier 3+: Add matching patterns ──
|
|
129
|
+
if (tier >= 3) {
|
|
130
|
+
const matchedPatterns = intent
|
|
131
|
+
? searchPatterns(intent)
|
|
132
|
+
: getAllPatterns().filter(p => p.domain === domainName);
|
|
133
|
+
|
|
134
|
+
for (const pattern of matchedPatterns) {
|
|
135
|
+
const cost = estimateTokens(pattern);
|
|
136
|
+
if (usedTokens + cost > budget) break;
|
|
137
|
+
result.patterns.push(pattern);
|
|
138
|
+
usedTokens += cost;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Tier 4: Full exploration — add remaining patterns ──
|
|
143
|
+
if (tier >= 4) {
|
|
144
|
+
const addedPatterns = new Set(result.patterns.map(p => p.name));
|
|
145
|
+
for (const pattern of getAllPatterns()) {
|
|
146
|
+
if (addedPatterns.has(pattern.name)) continue;
|
|
147
|
+
const cost = estimateTokens(pattern);
|
|
148
|
+
if (usedTokens + cost > budget) break;
|
|
149
|
+
result.patterns.push(pattern);
|
|
150
|
+
usedTokens += cost;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
result.estimatedTokens = usedTokens;
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Find an entry in the catalog by type name.
|
|
160
|
+
*/
|
|
161
|
+
function findEntry(catalog, typeName) {
|
|
162
|
+
if (catalog.entries.has(typeName)) return catalog.entries.get(typeName);
|
|
163
|
+
// Search by tag fallback
|
|
164
|
+
for (const entry of catalog.entries.values()) {
|
|
165
|
+
if (entry.type === typeName) return entry;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
package/decomposer.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent Decomposer — breaks complex intents into independent subtasks.
|
|
3
|
+
*
|
|
4
|
+
* When an intent describes multiple sections, areas, or features,
|
|
5
|
+
* the decomposer splits it into atomic generation units that can be
|
|
6
|
+
* generated independently and composed into a layout.
|
|
7
|
+
*
|
|
8
|
+
* Example:
|
|
9
|
+
* "settings page with profile, notifications, security, and billing"
|
|
10
|
+
* → 4 subtasks + a composition plan (Tabs layout)
|
|
11
|
+
*
|
|
12
|
+
* The decomposition is the single most important capability per the
|
|
13
|
+
* Software Factory Manifesto: "Get decomposition right and generation
|
|
14
|
+
* is almost trivial."
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { classifyIntent } from './domain-router.js';
|
|
18
|
+
|
|
19
|
+
// ── Section Detection ────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/** Connectors that separate sections in an intent */
|
|
22
|
+
const SECTION_SPLITTERS = /\band\b|\bwith\b|\bplus\b|\balso\b|,\s*(?:and\s+)?/gi;
|
|
23
|
+
|
|
24
|
+
/** Words that signal enumerated sections */
|
|
25
|
+
const SECTION_SIGNALS = [
|
|
26
|
+
/\b(\d+)\s+(sections?|areas?|parts?|panels?|columns?|cards?|tabs?|pages?|views?)\b/i,
|
|
27
|
+
/\bsections?:\s/i,
|
|
28
|
+
/\bincluding\b/i,
|
|
29
|
+
/\beach\s+(with|having|containing)\b/i,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/** Layout containers that imply multi-section structure */
|
|
33
|
+
const LAYOUT_KEYWORDS = {
|
|
34
|
+
tabs: { component: 'Tabs', child: 'Tab' },
|
|
35
|
+
sections: { component: 'Column', child: 'Card' },
|
|
36
|
+
columns: { component: 'Grid', child: 'Column' },
|
|
37
|
+
cards: { component: 'Grid', child: 'Card' },
|
|
38
|
+
panels: { component: 'Accordion', child: 'Panel' },
|
|
39
|
+
pages: { component: 'Tabs', child: 'Tab' },
|
|
40
|
+
areas: { component: 'Grid', child: 'Card' },
|
|
41
|
+
steps: { component: 'Steps', child: 'Step' },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Analyze an intent for decomposition potential.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} intent
|
|
48
|
+
* @returns {{ shouldDecompose: boolean, subtasks: { intent: string, label: string }[], layout: { component: string, child: string } | null, original: string }}
|
|
49
|
+
*/
|
|
50
|
+
export function decomposeIntent(intent) {
|
|
51
|
+
const trimmed = (intent || '').trim();
|
|
52
|
+
if (!trimmed) return { shouldDecompose: false, subtasks: [], layout: null, original: trimmed };
|
|
53
|
+
|
|
54
|
+
// ── Detect explicit section count ──
|
|
55
|
+
for (const pattern of SECTION_SIGNALS) {
|
|
56
|
+
if (pattern.test(trimmed)) {
|
|
57
|
+
const sections = extractSections(trimmed);
|
|
58
|
+
if (sections.length >= 2) {
|
|
59
|
+
const layout = detectLayout(trimmed, sections.length);
|
|
60
|
+
return {
|
|
61
|
+
shouldDecompose: true,
|
|
62
|
+
subtasks: sections,
|
|
63
|
+
layout,
|
|
64
|
+
original: trimmed,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Detect enumerated items ──
|
|
71
|
+
const sections = extractSections(trimmed);
|
|
72
|
+
if (sections.length >= 2) {
|
|
73
|
+
// 2+ distinct sections → decompose
|
|
74
|
+
const layout = detectLayout(trimmed, sections.length);
|
|
75
|
+
return {
|
|
76
|
+
shouldDecompose: true,
|
|
77
|
+
subtasks: sections,
|
|
78
|
+
layout,
|
|
79
|
+
original: trimmed,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Short or simple intent → don't decompose ──
|
|
84
|
+
return { shouldDecompose: false, subtasks: [], layout: null, original: trimmed };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract individual section descriptions from an intent.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} intent
|
|
91
|
+
* @returns {{ intent: string, label: string }[]}
|
|
92
|
+
*/
|
|
93
|
+
function extractSections(intent) {
|
|
94
|
+
// Find the "with X, Y, Z, and W" pattern
|
|
95
|
+
const withMatch = intent.match(/\bwith\s+(.+)$/i);
|
|
96
|
+
const payload = withMatch ? withMatch[1] : intent;
|
|
97
|
+
|
|
98
|
+
// Split on connectors
|
|
99
|
+
const parts = payload.split(SECTION_SPLITTERS)
|
|
100
|
+
.map(s => s.trim())
|
|
101
|
+
.filter(s => s.length > 2);
|
|
102
|
+
|
|
103
|
+
if (parts.length < 2) return [];
|
|
104
|
+
|
|
105
|
+
// Extract the base context (everything before "with")
|
|
106
|
+
const baseContext = withMatch ? intent.slice(0, withMatch.index).trim() : '';
|
|
107
|
+
const domain = classifyIntent(intent).domain;
|
|
108
|
+
|
|
109
|
+
return parts.map(part => {
|
|
110
|
+
// Build a self-contained subtask intent
|
|
111
|
+
const label = part.replace(/\b(section|area|panel|tab|page)\b/gi, '').trim();
|
|
112
|
+
const subtaskIntent = baseContext
|
|
113
|
+
? `${label} section for a ${baseContext}`
|
|
114
|
+
: `${label}`;
|
|
115
|
+
|
|
116
|
+
return { intent: subtaskIntent, label: capitalizeFirst(label) };
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Detect the best layout container for the decomposed sections.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} intent
|
|
124
|
+
* @param {number} sectionCount
|
|
125
|
+
* @returns {{ component: string, child: string }}
|
|
126
|
+
*/
|
|
127
|
+
function detectLayout(intent, sectionCount) {
|
|
128
|
+
const lower = intent.toLowerCase();
|
|
129
|
+
|
|
130
|
+
// Explicit layout keywords in the intent
|
|
131
|
+
for (const [keyword, layout] of Object.entries(LAYOUT_KEYWORDS)) {
|
|
132
|
+
if (lower.includes(keyword)) return layout;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Heuristic: settings/profile pages → Tabs
|
|
136
|
+
if (lower.includes('settings') || lower.includes('preferences') || lower.includes('account')) {
|
|
137
|
+
return { component: 'Tabs', child: 'Tab' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Heuristic: dashboard with metrics → Grid of Cards
|
|
141
|
+
if (lower.includes('dashboard') || lower.includes('overview') || lower.includes('stat')) {
|
|
142
|
+
return { component: 'Grid', child: 'Card' };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Default: small count → Tabs, large count → Grid
|
|
146
|
+
if (sectionCount <= 4) return { component: 'Tabs', child: 'Tab' };
|
|
147
|
+
return { component: 'Grid', child: 'Card' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Compose independently generated subtask results into a layout.
|
|
152
|
+
*
|
|
153
|
+
* @param {{ component: string, child: string }} layout
|
|
154
|
+
* @param {{ label: string, messages: object[] }[]} subtaskResults
|
|
155
|
+
* @returns {object[]} — A2UI messages for the composed layout
|
|
156
|
+
*/
|
|
157
|
+
export function composeSubtasks(layout, subtaskResults) {
|
|
158
|
+
const rootChildren = [];
|
|
159
|
+
const allComponents = [];
|
|
160
|
+
let idCounter = 0;
|
|
161
|
+
|
|
162
|
+
for (const { label, messages } of subtaskResults) {
|
|
163
|
+
const prefix = `s${++idCounter}`;
|
|
164
|
+
const subtaskComponents = messages?.[0]?.components || [];
|
|
165
|
+
|
|
166
|
+
// Re-prefix all component IDs to avoid collisions
|
|
167
|
+
const idMap = new Map();
|
|
168
|
+
const remapped = subtaskComponents.map(c => {
|
|
169
|
+
const newId = c.id === 'root' ? prefix : `${prefix}-${c.id}`;
|
|
170
|
+
idMap.set(c.id, newId);
|
|
171
|
+
return { ...c, id: newId };
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Fix child references
|
|
175
|
+
for (const c of remapped) {
|
|
176
|
+
if (c.children) {
|
|
177
|
+
c.children = c.children.map(id => idMap.get(id) || id);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Create the layout wrapper for this subtask
|
|
182
|
+
if (layout.child === 'Tab') {
|
|
183
|
+
// Tabs: wrap in a Tab with a label
|
|
184
|
+
const tabId = `${prefix}-tab`;
|
|
185
|
+
allComponents.push({ id: tabId, component: 'Tab', text: label, children: [prefix] });
|
|
186
|
+
rootChildren.push(tabId);
|
|
187
|
+
} else {
|
|
188
|
+
// Grid/Column: subtask root becomes a direct child
|
|
189
|
+
rootChildren.push(prefix);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
allComponents.push(...remapped);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Build the root layout
|
|
196
|
+
const root = {
|
|
197
|
+
id: 'root',
|
|
198
|
+
component: layout.component,
|
|
199
|
+
children: rootChildren,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
if (layout.component === 'Grid') {
|
|
203
|
+
root.columns = String(Math.min(subtaskResults.length, 4));
|
|
204
|
+
root.gap = 'md';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return [{
|
|
208
|
+
type: 'updateComponents',
|
|
209
|
+
surfaceId: 'default',
|
|
210
|
+
components: [root, ...allComponents],
|
|
211
|
+
}];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function capitalizeFirst(str) {
|
|
215
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
216
|
+
}
|