@adia-ai/a2ui-retrieval 0.6.4 → 0.6.7
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 +25 -0
- package/domain-router.js +362 -117
- package/embedding/chunk-embedding-retriever.js +47 -79
- package/embedding/embedding-provider.js +35 -71
- package/embedding/index.js +2 -10
- package/feedback/dialog-recorder.js +61 -145
- package/feedback/feedback-analyzer.js +46 -102
- package/feedback/feedback-store.js +91 -107
- package/feedback/feedback.js +36 -117
- package/feedback/gap-registry.js +40 -82
- package/feedback/index.js +14 -12
- package/index.d.ts +4 -0
- package/index.js +53 -16
- package/intent/clarity.js +61 -129
- package/intent/decomposer.js +51 -143
- package/intent/index.js +18 -14
- package/intent/intent-alignment.js +79 -150
- package/intent/intent-categorizer.js +34 -62
- package/intent/intent-gate.js +43 -102
- package/intent/prompt-analyzer.js +68 -126
- package/package.json +4 -2
- package/wiring-catalog.js +95 -146
- package/embedding/chunk-embedding-retriever.ts +0 -156
- package/embedding/embedding-provider.ts +0 -111
- package/embedding/index.ts +0 -10
- package/feedback/dialog-recorder.ts +0 -172
- package/feedback/feedback-analyzer.ts +0 -250
- package/feedback/feedback-store.ts +0 -229
- package/feedback/feedback.ts +0 -201
- package/feedback/gap-registry.ts +0 -137
- package/feedback/index.ts +0 -14
- package/intent/clarity.ts +0 -224
- package/intent/decomposer.ts +0 -229
- package/intent/index.ts +0 -20
- package/intent/intent-alignment.ts +0 -267
- package/intent/intent-categorizer.ts +0 -104
- package/intent/intent-gate.ts +0 -151
- package/intent/prompt-analyzer.ts +0 -231
package/intent/decomposer.ts
DELETED
|
@@ -1,229 +0,0 @@
|
|
|
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: Record<string, { component: string; child: string }> = {
|
|
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
|
-
export type Subtask = { intent: string; label: string };
|
|
45
|
-
export type LayoutInfo = { component: string; child: string };
|
|
46
|
-
|
|
47
|
-
export type DecomposeResult = {
|
|
48
|
-
shouldDecompose: boolean;
|
|
49
|
-
subtasks: Subtask[];
|
|
50
|
-
layout: LayoutInfo | null;
|
|
51
|
-
original: string;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Analyze an intent for decomposition potential.
|
|
56
|
-
*/
|
|
57
|
-
export function decomposeIntent(intent: string): DecomposeResult {
|
|
58
|
-
const trimmed = (intent || '').trim();
|
|
59
|
-
if (!trimmed) return { shouldDecompose: false, subtasks: [], layout: null, original: trimmed };
|
|
60
|
-
|
|
61
|
-
// ── Detect explicit section count ──
|
|
62
|
-
for (const pattern of SECTION_SIGNALS) {
|
|
63
|
-
if (pattern.test(trimmed)) {
|
|
64
|
-
const sections = extractSections(trimmed);
|
|
65
|
-
if (sections.length >= 2) {
|
|
66
|
-
const layout = detectLayout(trimmed, sections.length);
|
|
67
|
-
return {
|
|
68
|
-
shouldDecompose: true,
|
|
69
|
-
subtasks: sections,
|
|
70
|
-
layout,
|
|
71
|
-
original: trimmed,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ── Detect enumerated items ──
|
|
78
|
-
const sections = extractSections(trimmed);
|
|
79
|
-
if (sections.length >= 2) {
|
|
80
|
-
// 2+ distinct sections → decompose
|
|
81
|
-
const layout = detectLayout(trimmed, sections.length);
|
|
82
|
-
return {
|
|
83
|
-
shouldDecompose: true,
|
|
84
|
-
subtasks: sections,
|
|
85
|
-
layout,
|
|
86
|
-
original: trimmed,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ── Short or simple intent → don't decompose ──
|
|
91
|
-
return { shouldDecompose: false, subtasks: [], layout: null, original: trimmed };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Extract individual section descriptions from an intent.
|
|
96
|
-
*/
|
|
97
|
-
function extractSections(intent: string): Subtask[] {
|
|
98
|
-
// Find the "with X, Y, Z, and W" pattern
|
|
99
|
-
const withMatch = intent.match(/\bwith\s+(.+)$/i);
|
|
100
|
-
const payload = withMatch ? withMatch[1] : intent;
|
|
101
|
-
|
|
102
|
-
// Split on connectors
|
|
103
|
-
const parts = payload.split(SECTION_SPLITTERS)
|
|
104
|
-
.map(s => s.trim())
|
|
105
|
-
.filter(s => s.length > 2);
|
|
106
|
-
|
|
107
|
-
if (parts.length < 2) return [];
|
|
108
|
-
|
|
109
|
-
// Extract the base context (everything before "with")
|
|
110
|
-
const baseContext = withMatch ? intent.slice(0, withMatch.index).trim() : '';
|
|
111
|
-
const domain = classifyIntent(intent).domain;
|
|
112
|
-
void domain; // used for context only in the original
|
|
113
|
-
|
|
114
|
-
return parts.map(part => {
|
|
115
|
-
// Build a self-contained subtask intent
|
|
116
|
-
const label = part.replace(/\b(section|area|panel|tab|page)\b/gi, '').trim();
|
|
117
|
-
const subtaskIntent = baseContext
|
|
118
|
-
? `${label} section for a ${baseContext}`
|
|
119
|
-
: `${label}`;
|
|
120
|
-
|
|
121
|
-
return { intent: subtaskIntent, label: capitalizeFirst(label) };
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Detect the best layout container for the decomposed sections.
|
|
127
|
-
*/
|
|
128
|
-
function detectLayout(intent: string, sectionCount: number): LayoutInfo {
|
|
129
|
-
const lower = intent.toLowerCase();
|
|
130
|
-
|
|
131
|
-
// Explicit layout keywords in the intent
|
|
132
|
-
for (const [keyword, layout] of Object.entries(LAYOUT_KEYWORDS)) {
|
|
133
|
-
if (lower.includes(keyword)) return layout;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Heuristic: settings/profile pages → Tabs
|
|
137
|
-
if (lower.includes('settings') || lower.includes('preferences') || lower.includes('account')) {
|
|
138
|
-
return { component: 'Tabs', child: 'Tab' };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Heuristic: dashboard with metrics → Grid of Cards
|
|
142
|
-
if (lower.includes('dashboard') || lower.includes('overview') || lower.includes('stat')) {
|
|
143
|
-
return { component: 'Grid', child: 'Card' };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Default: small count → Tabs, large count → Grid
|
|
147
|
-
if (sectionCount <= 4) return { component: 'Tabs', child: 'Tab' };
|
|
148
|
-
return { component: 'Grid', child: 'Card' };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
type SubtaskResult = {
|
|
152
|
-
label: string;
|
|
153
|
-
messages: Array<{ components?: ComponentItem[] }>;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
type ComponentItem = {
|
|
157
|
-
id: string;
|
|
158
|
-
component?: string;
|
|
159
|
-
children?: string[];
|
|
160
|
-
text?: string;
|
|
161
|
-
columns?: string;
|
|
162
|
-
gap?: string;
|
|
163
|
-
[key: string]: unknown;
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Compose independently generated subtask results into a layout.
|
|
168
|
-
* Returns A2UI messages for the composed layout.
|
|
169
|
-
*/
|
|
170
|
-
export function composeSubtasks(layout: LayoutInfo, subtaskResults: SubtaskResult[]): object[] {
|
|
171
|
-
const rootChildren: string[] = [];
|
|
172
|
-
const allComponents: ComponentItem[] = [];
|
|
173
|
-
let idCounter = 0;
|
|
174
|
-
|
|
175
|
-
for (const { label, messages } of subtaskResults) {
|
|
176
|
-
const prefix = `s${++idCounter}`;
|
|
177
|
-
const subtaskComponents: ComponentItem[] = messages?.[0]?.components ?? [];
|
|
178
|
-
|
|
179
|
-
// Re-prefix all component IDs to avoid collisions
|
|
180
|
-
const idMap = new Map<string, string>();
|
|
181
|
-
const remapped = subtaskComponents.map(c => {
|
|
182
|
-
const newId = c.id === 'root' ? prefix : `${prefix}-${c.id}`;
|
|
183
|
-
idMap.set(c.id, newId);
|
|
184
|
-
return { ...c, id: newId };
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// Fix child references
|
|
188
|
-
for (const c of remapped) {
|
|
189
|
-
if (c.children) {
|
|
190
|
-
c.children = c.children.map(id => idMap.get(id) ?? id);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Create the layout wrapper for this subtask
|
|
195
|
-
if (layout.child === 'Tab') {
|
|
196
|
-
// Tabs: wrap in a Tab with a label
|
|
197
|
-
const tabId = `${prefix}-tab`;
|
|
198
|
-
allComponents.push({ id: tabId, component: 'Tab', text: label, children: [prefix] });
|
|
199
|
-
rootChildren.push(tabId);
|
|
200
|
-
} else {
|
|
201
|
-
// Grid/Column: subtask root becomes a direct child
|
|
202
|
-
rootChildren.push(prefix);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
allComponents.push(...remapped);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Build the root layout
|
|
209
|
-
const root: ComponentItem = {
|
|
210
|
-
id: 'root',
|
|
211
|
-
component: layout.component,
|
|
212
|
-
children: rootChildren,
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
if (layout.component === 'Grid') {
|
|
216
|
-
root['columns'] = String(Math.min(subtaskResults.length, 4));
|
|
217
|
-
root['gap'] = 'md';
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return [{
|
|
221
|
-
type: 'updateComponents',
|
|
222
|
-
surfaceId: 'default',
|
|
223
|
-
components: [root, ...allComponents],
|
|
224
|
-
}];
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function capitalizeFirst(str: string): string {
|
|
228
|
-
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
229
|
-
}
|
package/intent/index.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @adia-ai/a2ui-retrieval/intent — intent classification + decomposition surface.
|
|
3
|
-
*
|
|
4
|
-
* Re-exports the intent-shaped retrieval primitives. Importers can reach
|
|
5
|
-
* individual files directly (e.g. `@adia-ai/a2ui-retrieval/intent/clarity`)
|
|
6
|
-
* or pull the bundle via this barrel.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export { extractExpectations, verifyAlignment, checkIntentAlignment } from './intent-alignment.js';
|
|
10
|
-
export type { Expectations, AlignmentCheck, AlignmentResult, QuantityExpectation } from './intent-alignment.js';
|
|
11
|
-
export { categorizeIntent } from './intent-categorizer.js';
|
|
12
|
-
export type { IntentCategory } from './intent-categorizer.js';
|
|
13
|
-
export { isConversational } from './intent-gate.js';
|
|
14
|
-
export type { IntentGateResult } from './intent-gate.js';
|
|
15
|
-
export { analyzePrompt, formatAnalysisForPrompt } from './prompt-analyzer.js';
|
|
16
|
-
export type { PromptAnalysis } from './prompt-analyzer.js';
|
|
17
|
-
export { decomposeIntent, composeSubtasks } from './decomposer.js';
|
|
18
|
-
export type { DecomposeResult, Subtask, LayoutInfo } from './decomposer.js';
|
|
19
|
-
export { assessClarity } from './clarity.js';
|
|
20
|
-
export type { ClarityResult, ClarityDimensions, ClarityQuestion } from './clarity.js';
|
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Intent Alignment Verification — checks whether generated output
|
|
3
|
-
* actually contains what the user asked for.
|
|
4
|
-
*
|
|
5
|
-
* Extracts expectations from the intent (field labels, component types,
|
|
6
|
-
* quantities, action verbs) and verifies them against the A2UI output.
|
|
7
|
-
*
|
|
8
|
-
* Returns a structured alignment report with per-expectation pass/fail,
|
|
9
|
-
* an overall alignment score (0-1), and specific gaps.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
// ── Expectation Extractors ──────────────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
/** Field/label expectations — "email", "password", "name", etc. */
|
|
15
|
-
const FIELD_PATTERNS = [
|
|
16
|
-
/\b(email|password|username|name|phone|address|city|state|zip|country|company|title|role|bio|url|website|date|time|description|message|subject|amount|price|quantity|search|notes?)\b/gi,
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
/** Component type expectations — "button", "form", "table", etc. */
|
|
20
|
-
const COMPONENT_PATTERNS = [
|
|
21
|
-
/\b(button|form|table|chart|card|avatar|badge|alert|modal|drawer|tabs?|sidebar|navbar|breadcrumb|pagination|progress|slider|toggle|checkbox|radio|dropdown|select|upload|calendar|timeline|steps?|accordion)\b/gi,
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
/** Quantity expectations — "3 cards", "two columns", etc. */
|
|
25
|
-
const QUANTITY_MAP: Record<string, number> = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10 };
|
|
26
|
-
const QUANTITY_PATTERN = /\b(\d+|one|two|three|four|five|six|seven|eight|nine|ten)\s+(cards?|columns?|items?|fields?|buttons?|sections?|rows?|tiles?|steps?|tabs?|metrics?|stats?)\b/gi;
|
|
27
|
-
|
|
28
|
-
/** Action expectations — "submit", "cancel", "delete", etc. */
|
|
29
|
-
const ACTION_PATTERNS = [
|
|
30
|
-
/\b(submit|cancel|save|delete|edit|close|open|search|filter|sort|login|signup|register|checkout|confirm|reset|send|upload|download|share|copy|print)\b/gi,
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
/** Specific content expectations — quoted strings, dollar amounts, percentages */
|
|
34
|
-
const CONTENT_PATTERNS = [
|
|
35
|
-
/["']([^"']+)["']/g, // quoted strings
|
|
36
|
-
/\$[\d,.]+/g, // dollar amounts
|
|
37
|
-
/\d+%/g, // percentages
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
export type QuantityExpectation = { count: number; type: string };
|
|
41
|
-
|
|
42
|
-
export type Expectations = {
|
|
43
|
-
fields: string[];
|
|
44
|
-
componentTypes: string[];
|
|
45
|
-
quantities: QuantityExpectation[];
|
|
46
|
-
actions: string[];
|
|
47
|
-
content: string[];
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export type AlignmentCheck = {
|
|
51
|
-
category: string;
|
|
52
|
-
expected: string;
|
|
53
|
-
found: boolean;
|
|
54
|
-
detail: string;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export type AlignmentResult = {
|
|
58
|
-
score: number;
|
|
59
|
-
checks: AlignmentCheck[];
|
|
60
|
-
gaps: string[];
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Extract expectations from a natural language intent.
|
|
65
|
-
*/
|
|
66
|
-
export function extractExpectations(intent: string): Expectations {
|
|
67
|
-
const lower = intent.toLowerCase();
|
|
68
|
-
|
|
69
|
-
// Fields
|
|
70
|
-
const fields: string[] = [];
|
|
71
|
-
for (const pattern of FIELD_PATTERNS) {
|
|
72
|
-
pattern.lastIndex = 0;
|
|
73
|
-
let match;
|
|
74
|
-
while ((match = pattern.exec(lower))) {
|
|
75
|
-
const f = match[1].toLowerCase();
|
|
76
|
-
if (!fields.includes(f)) fields.push(f);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Component types
|
|
81
|
-
const componentTypes: string[] = [];
|
|
82
|
-
for (const pattern of COMPONENT_PATTERNS) {
|
|
83
|
-
pattern.lastIndex = 0;
|
|
84
|
-
let match;
|
|
85
|
-
while ((match = pattern.exec(lower))) {
|
|
86
|
-
const t = match[1].toLowerCase();
|
|
87
|
-
if (!componentTypes.includes(t)) componentTypes.push(t);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Quantities
|
|
92
|
-
const quantities: QuantityExpectation[] = [];
|
|
93
|
-
QUANTITY_PATTERN.lastIndex = 0;
|
|
94
|
-
let qMatch;
|
|
95
|
-
while ((qMatch = QUANTITY_PATTERN.exec(lower))) {
|
|
96
|
-
const raw = qMatch[1];
|
|
97
|
-
const num = QUANTITY_MAP[raw] ?? parseInt(raw, 10);
|
|
98
|
-
const type = qMatch[2].replace(/s$/, ''); // singularize
|
|
99
|
-
if (!isNaN(num)) quantities.push({ count: num, type });
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Actions
|
|
103
|
-
const actions: string[] = [];
|
|
104
|
-
for (const pattern of ACTION_PATTERNS) {
|
|
105
|
-
pattern.lastIndex = 0;
|
|
106
|
-
let match;
|
|
107
|
-
while ((match = pattern.exec(lower))) {
|
|
108
|
-
const a = match[1].toLowerCase();
|
|
109
|
-
if (!actions.includes(a)) actions.push(a);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Specific content
|
|
114
|
-
const content: string[] = [];
|
|
115
|
-
for (const pattern of CONTENT_PATTERNS) {
|
|
116
|
-
pattern.lastIndex = 0;
|
|
117
|
-
let match;
|
|
118
|
-
while ((match = pattern.exec(intent))) {
|
|
119
|
-
content.push(match[1] ?? match[0]);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return { fields, componentTypes, quantities, actions, content };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ── A2UI Type Mapping ───────────────────────────────────────────────────
|
|
127
|
-
|
|
128
|
-
/** Map intent keywords to A2UI component types */
|
|
129
|
-
const TYPE_MAP: Record<string, string[]> = {
|
|
130
|
-
button: ['Button'],
|
|
131
|
-
form: ['FormContainer', 'TextField', 'Column'],
|
|
132
|
-
table: ['Table'],
|
|
133
|
-
chart: ['Chart'],
|
|
134
|
-
card: ['Card'],
|
|
135
|
-
avatar: ['Avatar'],
|
|
136
|
-
badge: ['Badge'],
|
|
137
|
-
alert: ['Alert'],
|
|
138
|
-
modal: ['Modal', 'Dialog'],
|
|
139
|
-
drawer: ['Drawer'],
|
|
140
|
-
tab: ['Tabs', 'Tab'],
|
|
141
|
-
sidebar: ['Sidebar'],
|
|
142
|
-
navbar: ['Nav'],
|
|
143
|
-
breadcrumb: ['Breadcrumb'],
|
|
144
|
-
pagination: ['Pagination'],
|
|
145
|
-
progress: ['Progress'],
|
|
146
|
-
slider: ['Slider'],
|
|
147
|
-
toggle: ['Toggle'],
|
|
148
|
-
checkbox: ['CheckBox'],
|
|
149
|
-
radio: ['Radio'],
|
|
150
|
-
dropdown: ['ChoicePicker', 'Select'],
|
|
151
|
-
select: ['ChoicePicker', 'Select'],
|
|
152
|
-
upload: ['Upload'],
|
|
153
|
-
calendar: ['CalendarPicker', 'DateTimeInput'],
|
|
154
|
-
timeline: ['Timeline'],
|
|
155
|
-
step: ['Steps'],
|
|
156
|
-
accordion: ['Accordion'],
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
type ComponentRecord = {
|
|
160
|
-
component?: string;
|
|
161
|
-
textContent?: string;
|
|
162
|
-
text?: string;
|
|
163
|
-
label?: string;
|
|
164
|
-
placeholder?: string;
|
|
165
|
-
description?: string;
|
|
166
|
-
name?: string;
|
|
167
|
-
[key: string]: unknown;
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Verify generated A2UI output against extracted expectations.
|
|
172
|
-
*/
|
|
173
|
-
export function verifyAlignment(components: ComponentRecord[], expectations: Expectations): AlignmentResult {
|
|
174
|
-
const checks: AlignmentCheck[] = [];
|
|
175
|
-
const gaps: string[] = [];
|
|
176
|
-
|
|
177
|
-
// Collect all text content and properties from components
|
|
178
|
-
const allText = components.map(c => {
|
|
179
|
-
const texts = [c['textContent'], c['text'], c['label'], c['placeholder'], c['description'], c['name']];
|
|
180
|
-
return texts.filter(Boolean).join(' ').toLowerCase();
|
|
181
|
-
}).join(' ');
|
|
182
|
-
|
|
183
|
-
const allTypes = new Set(components.map(c => c.component));
|
|
184
|
-
|
|
185
|
-
// ── Check fields ──
|
|
186
|
-
for (const field of expectations.fields) {
|
|
187
|
-
const found = allText.includes(field) ||
|
|
188
|
-
components.some(c => (c['label'] ?? '').toLowerCase().includes(field) ||
|
|
189
|
-
(c['placeholder'] ?? '').toLowerCase().includes(field) ||
|
|
190
|
-
(c['name'] ?? '').toLowerCase().includes(field));
|
|
191
|
-
checks.push({
|
|
192
|
-
category: 'field',
|
|
193
|
-
expected: field,
|
|
194
|
-
found,
|
|
195
|
-
detail: found ? `Found "${field}" in component properties` : `Missing field: "${field}"`,
|
|
196
|
-
});
|
|
197
|
-
if (!found) gaps.push(`Missing field "${field}"`);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ── Check component types ──
|
|
201
|
-
for (const type of expectations.componentTypes) {
|
|
202
|
-
const mappedTypes = TYPE_MAP[type] ?? [type.charAt(0).toUpperCase() + type.slice(1)];
|
|
203
|
-
const found = mappedTypes.some(t => allTypes.has(t));
|
|
204
|
-
checks.push({
|
|
205
|
-
category: 'componentType',
|
|
206
|
-
expected: type,
|
|
207
|
-
found,
|
|
208
|
-
detail: found ? `Found ${type} component` : `Missing component type: ${type}`,
|
|
209
|
-
});
|
|
210
|
-
if (!found) gaps.push(`Missing ${type} component`);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// ── Check quantities ──
|
|
214
|
-
for (const { count, type } of expectations.quantities) {
|
|
215
|
-
const mappedTypes = TYPE_MAP[type] ?? [type.charAt(0).toUpperCase() + type.slice(1)];
|
|
216
|
-
const actual = components.filter(c => mappedTypes.some(t => c.component === t)).length;
|
|
217
|
-
const found = actual >= count;
|
|
218
|
-
checks.push({
|
|
219
|
-
category: 'quantity',
|
|
220
|
-
expected: `${count} ${type}(s)`,
|
|
221
|
-
found,
|
|
222
|
-
detail: found ? `Found ${actual} ${type}(s) (expected ${count})` : `Only ${actual} ${type}(s), expected ${count}`,
|
|
223
|
-
});
|
|
224
|
-
if (!found) gaps.push(`Expected ${count} ${type}(s), found ${actual}`);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// ── Check actions ──
|
|
228
|
-
for (const action of expectations.actions) {
|
|
229
|
-
const found = allText.includes(action) ||
|
|
230
|
-
components.some(c => c.component === 'Button' && (c['text'] ?? '').toLowerCase().includes(action));
|
|
231
|
-
checks.push({
|
|
232
|
-
category: 'action',
|
|
233
|
-
expected: action,
|
|
234
|
-
found,
|
|
235
|
-
detail: found ? `Found "${action}" action` : `Missing action: "${action}"`,
|
|
236
|
-
});
|
|
237
|
-
if (!found) gaps.push(`Missing "${action}" action`);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// ── Check specific content ──
|
|
241
|
-
for (const text of expectations.content) {
|
|
242
|
-
const found = allText.includes(text.toLowerCase());
|
|
243
|
-
checks.push({
|
|
244
|
-
category: 'content',
|
|
245
|
-
expected: text,
|
|
246
|
-
found,
|
|
247
|
-
detail: found ? `Found content "${text}"` : `Missing content: "${text}"`,
|
|
248
|
-
});
|
|
249
|
-
if (!found) gaps.push(`Missing content "${text}"`);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// ── Score ──
|
|
253
|
-
const total = checks.length;
|
|
254
|
-
const passed = checks.filter(c => c.found).length;
|
|
255
|
-
const score = total > 0 ? Math.round((passed / total) * 100) / 100 : 1;
|
|
256
|
-
|
|
257
|
-
return { score, checks, gaps };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Full intent alignment check — extract + verify in one call.
|
|
262
|
-
*/
|
|
263
|
-
export function checkIntentAlignment(intent: string, components: ComponentRecord[]): AlignmentResult & { expectations: Expectations } {
|
|
264
|
-
const expectations = extractExpectations(intent);
|
|
265
|
-
const result = verifyAlignment(components, expectations);
|
|
266
|
-
return { ...result, expectations };
|
|
267
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Intent Categorizer
|
|
3
|
-
*
|
|
4
|
-
* Maps free-text intents to a taxonomy of UI categories using keyword matching.
|
|
5
|
-
* Used by the feedback analyzer to aggregate metrics per intent type.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
type CategoryRule = {
|
|
9
|
-
category: string;
|
|
10
|
-
keywords: string[];
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const CATEGORY_RULES: CategoryRule[] = [
|
|
14
|
-
// Form categories
|
|
15
|
-
{ category: 'form/login', keywords: ['login', 'sign in', 'signin', 'log in', 'authentication'] },
|
|
16
|
-
{ category: 'form/signup', keywords: ['signup', 'sign up', 'register', 'registration', 'create account', 'onboarding'] },
|
|
17
|
-
{ category: 'form/contact', keywords: ['contact', 'contact us', 'reach out', 'get in touch', 'enquiry', 'inquiry'] },
|
|
18
|
-
{ category: 'form/settings', keywords: ['settings', 'preferences', 'config', 'configuration', 'account settings', 'profile edit'] },
|
|
19
|
-
{ category: 'form/checkout', keywords: ['checkout', 'check out', 'payment', 'billing', 'purchase'] },
|
|
20
|
-
|
|
21
|
-
// Data categories
|
|
22
|
-
{ category: 'data/table', keywords: ['table', 'data table', 'spreadsheet', 'grid view', 'list view', 'datagrid'] },
|
|
23
|
-
{ category: 'data/dashboard', keywords: ['dashboard', 'kpi', 'metrics', 'analytics', 'overview', 'summary panel', 'stats'] },
|
|
24
|
-
{ category: 'data/chart', keywords: ['chart', 'graph', 'visualization', 'pie chart', 'bar chart', 'line chart', 'histogram'] },
|
|
25
|
-
|
|
26
|
-
// Layout categories
|
|
27
|
-
{ category: 'layout/landing', keywords: ['landing', 'landing page', 'homepage', 'hero', 'splash'] },
|
|
28
|
-
{ category: 'layout/profile', keywords: ['profile', 'user profile', 'avatar', 'bio', 'about me'] },
|
|
29
|
-
{ category: 'layout/pricing', keywords: ['pricing', 'pricing table', 'plans', 'subscription', 'tier'] },
|
|
30
|
-
|
|
31
|
-
// Navigation categories
|
|
32
|
-
{ category: 'nav/sidebar', keywords: ['sidebar', 'side nav', 'navigation menu', 'drawer', 'side panel'] },
|
|
33
|
-
{ category: 'nav/tabs', keywords: ['tabs', 'tab bar', 'tabbed', 'tab navigation', 'tab panel'] },
|
|
34
|
-
|
|
35
|
-
// Agent categories
|
|
36
|
-
{ category: 'agent/chat', keywords: ['chat', 'chatbot', 'messenger', 'conversation', 'messaging', 'chat interface'] },
|
|
37
|
-
{ category: 'agent/notification', keywords: ['notification', 'alert', 'toast', 'snackbar', 'banner', 'announcement'] },
|
|
38
|
-
|
|
39
|
-
// Content categories
|
|
40
|
-
{ category: 'content/blog', keywords: ['blog', 'article', 'post', 'news', 'editorial', 'content feed'] },
|
|
41
|
-
{ category: 'content/faq', keywords: ['faq', 'frequently asked', 'questions', 'help center', 'knowledge base', 'accordion'] },
|
|
42
|
-
|
|
43
|
-
// Commerce categories
|
|
44
|
-
{ category: 'commerce/product', keywords: ['product', 'product card', 'product page', 'product detail', 'item detail', 'catalog'] },
|
|
45
|
-
{ category: 'commerce/cart', keywords: ['cart', 'shopping cart', 'basket', 'bag'] },
|
|
46
|
-
{ category: 'commerce/order', keywords: ['order', 'order history', 'order summary', 'receipt', 'invoice', 'order tracking'] },
|
|
47
|
-
|
|
48
|
-
// Workflow categories
|
|
49
|
-
{ category: 'workflow/wizard', keywords: ['wizard', 'stepper', 'multi-step', 'step form', 'onboard flow', 'setup wizard'] },
|
|
50
|
-
{ category: 'workflow/kanban', keywords: ['kanban', 'board', 'task board', 'trello', 'project board', 'drag and drop'] },
|
|
51
|
-
|
|
52
|
-
// Status categories
|
|
53
|
-
{ category: 'status/error', keywords: ['error', 'error page', '404', '500', 'not found', 'something went wrong'] },
|
|
54
|
-
{ category: 'status/empty', keywords: ['empty state', 'no results', 'no data', 'zero state', 'blank slate'] },
|
|
55
|
-
{ category: 'status/loading', keywords: ['loading', 'skeleton', 'spinner', 'progress', 'placeholder'] },
|
|
56
|
-
];
|
|
57
|
-
|
|
58
|
-
export type IntentCategory = {
|
|
59
|
-
category: string;
|
|
60
|
-
confidence: number;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Categorize a free-text intent into a UI taxonomy category.
|
|
65
|
-
*/
|
|
66
|
-
export function categorizeIntent(intent: string): IntentCategory {
|
|
67
|
-
if (!intent || typeof intent !== 'string') {
|
|
68
|
-
return { category: 'other', confidence: 0 };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const lower = intent.toLowerCase().trim();
|
|
72
|
-
let bestCategory = 'other';
|
|
73
|
-
let bestScore = 0;
|
|
74
|
-
|
|
75
|
-
for (const rule of CATEGORY_RULES) {
|
|
76
|
-
let matchCount = 0;
|
|
77
|
-
let longestMatch = 0;
|
|
78
|
-
|
|
79
|
-
for (const kw of rule.keywords) {
|
|
80
|
-
if (lower.includes(kw)) {
|
|
81
|
-
matchCount++;
|
|
82
|
-
longestMatch = Math.max(longestMatch, kw.length);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (matchCount === 0) continue;
|
|
87
|
-
|
|
88
|
-
// Score: multi-word keyword matches get higher confidence,
|
|
89
|
-
// more matches within a category = higher confidence
|
|
90
|
-
const kwLenBonus = longestMatch / lower.length; // longer keyword relative to intent = more specific
|
|
91
|
-
const multiMatchBonus = Math.min(matchCount * 0.15, 0.3);
|
|
92
|
-
const score = 0.5 + kwLenBonus * 0.3 + multiMatchBonus;
|
|
93
|
-
|
|
94
|
-
if (score > bestScore) {
|
|
95
|
-
bestScore = score;
|
|
96
|
-
bestCategory = rule.category;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
category: bestCategory,
|
|
102
|
-
confidence: Math.min(Math.round(bestScore * 100) / 100, 1),
|
|
103
|
-
};
|
|
104
|
-
}
|