@adia-ai/a2ui-retrieval 0.6.4 → 0.6.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 +21 -0
- package/domain-router.js +362 -117
- package/embedding/chunk-embedding-retriever.js +47 -79
- package/embedding/chunk-embedding-retriever.ts +2 -2
- package/embedding/embedding-provider.js +35 -71
- package/embedding/index.js +2 -10
- package/feedback/dialog-recorder.js +61 -145
- package/feedback/dialog-recorder.ts +11 -8
- package/feedback/feedback-analyzer.js +46 -102
- package/feedback/feedback-analyzer.ts +2 -2
- package/feedback/feedback-store.js +91 -107
- package/feedback/feedback-store.ts +1 -1
- package/feedback/feedback.js +36 -117
- package/feedback/gap-registry.js +40 -82
- package/feedback/index.js +14 -12
- package/index.js +53 -16
- package/intent/clarity.js +61 -129
- package/intent/decomposer.js +51 -143
- package/intent/decomposer.ts +1 -1
- package/intent/index.js +18 -14
- package/intent/intent-alignment.js +79 -150
- package/intent/intent-alignment.ts +5 -5
- package/intent/intent-categorizer.js +34 -62
- package/intent/intent-gate.js +43 -102
- package/intent/prompt-analyzer.js +68 -126
- package/intent/prompt-analyzer.ts +1 -1
- package/package.json +1 -1
- package/wiring-catalog.js +95 -146
|
@@ -1,243 +1,172 @@
|
|
|
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
1
|
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
|
|
2
|
+
/\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
3
|
];
|
|
18
|
-
|
|
19
|
-
/** Component type expectations — "button", "form", "table", etc. */
|
|
20
4
|
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
|
|
5
|
+
/\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
6
|
];
|
|
23
|
-
|
|
24
|
-
/** Quantity expectations — "3 cards", "two columns", etc. */
|
|
25
7
|
const QUANTITY_MAP = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10 };
|
|
26
8
|
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
9
|
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
|
|
10
|
+
/\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
11
|
];
|
|
32
|
-
|
|
33
|
-
/** Specific content expectations — quoted strings, dollar amounts, percentages */
|
|
34
12
|
const CONTENT_PATTERNS = [
|
|
35
|
-
/["']([^"']+)["']/g,
|
|
36
|
-
|
|
37
|
-
|
|
13
|
+
/["']([^"']+)["']/g,
|
|
14
|
+
// quoted strings
|
|
15
|
+
/\$[\d,.]+/g,
|
|
16
|
+
// dollar amounts
|
|
17
|
+
/\d+%/g
|
|
18
|
+
// percentages
|
|
38
19
|
];
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Extract expectations from a natural language intent.
|
|
42
|
-
*
|
|
43
|
-
* @param {string} intent
|
|
44
|
-
* @returns {{ fields: string[], componentTypes: string[], quantities: { count: number, type: string }[], actions: string[], content: string[] }}
|
|
45
|
-
*/
|
|
46
|
-
export function extractExpectations(intent) {
|
|
20
|
+
function extractExpectations(intent) {
|
|
47
21
|
const lower = intent.toLowerCase();
|
|
48
|
-
|
|
49
|
-
// Fields
|
|
50
22
|
const fields = [];
|
|
51
23
|
for (const pattern of FIELD_PATTERNS) {
|
|
52
24
|
pattern.lastIndex = 0;
|
|
53
25
|
let match;
|
|
54
|
-
while (
|
|
55
|
-
const f = match[1].toLowerCase();
|
|
26
|
+
while (match = pattern.exec(lower)) {
|
|
27
|
+
const f = (match[1] ?? "").toLowerCase();
|
|
56
28
|
if (!fields.includes(f)) fields.push(f);
|
|
57
29
|
}
|
|
58
30
|
}
|
|
59
|
-
|
|
60
|
-
// Component types
|
|
61
31
|
const componentTypes = [];
|
|
62
32
|
for (const pattern of COMPONENT_PATTERNS) {
|
|
63
33
|
pattern.lastIndex = 0;
|
|
64
34
|
let match;
|
|
65
|
-
while (
|
|
66
|
-
const t = match[1].toLowerCase();
|
|
35
|
+
while (match = pattern.exec(lower)) {
|
|
36
|
+
const t = (match[1] ?? "").toLowerCase();
|
|
67
37
|
if (!componentTypes.includes(t)) componentTypes.push(t);
|
|
68
38
|
}
|
|
69
39
|
}
|
|
70
|
-
|
|
71
|
-
// Quantities
|
|
72
40
|
const quantities = [];
|
|
73
41
|
QUANTITY_PATTERN.lastIndex = 0;
|
|
74
42
|
let qMatch;
|
|
75
|
-
while (
|
|
76
|
-
const
|
|
77
|
-
const
|
|
43
|
+
while (qMatch = QUANTITY_PATTERN.exec(lower)) {
|
|
44
|
+
const raw = qMatch[1] ?? "";
|
|
45
|
+
const num = QUANTITY_MAP[raw] ?? parseInt(raw, 10);
|
|
46
|
+
const type = (qMatch[2] ?? "").replace(/s$/, "");
|
|
78
47
|
if (!isNaN(num)) quantities.push({ count: num, type });
|
|
79
48
|
}
|
|
80
|
-
|
|
81
|
-
// Actions
|
|
82
49
|
const actions = [];
|
|
83
50
|
for (const pattern of ACTION_PATTERNS) {
|
|
84
51
|
pattern.lastIndex = 0;
|
|
85
52
|
let match;
|
|
86
|
-
while (
|
|
87
|
-
const a = match[1].toLowerCase();
|
|
53
|
+
while (match = pattern.exec(lower)) {
|
|
54
|
+
const a = (match[1] ?? "").toLowerCase();
|
|
88
55
|
if (!actions.includes(a)) actions.push(a);
|
|
89
56
|
}
|
|
90
57
|
}
|
|
91
|
-
|
|
92
|
-
// Specific content
|
|
93
58
|
const content = [];
|
|
94
59
|
for (const pattern of CONTENT_PATTERNS) {
|
|
95
60
|
pattern.lastIndex = 0;
|
|
96
61
|
let match;
|
|
97
|
-
while (
|
|
98
|
-
content.push(match[1]
|
|
62
|
+
while (match = pattern.exec(intent)) {
|
|
63
|
+
content.push(match[1] ?? match[0]);
|
|
99
64
|
}
|
|
100
65
|
}
|
|
101
|
-
|
|
102
66
|
return { fields, componentTypes, quantities, actions, content };
|
|
103
67
|
}
|
|
104
|
-
|
|
105
|
-
// ── A2UI Type Mapping ───────────────────────────────────────────────────
|
|
106
|
-
|
|
107
|
-
/** Map intent keywords to A2UI component types */
|
|
108
68
|
const TYPE_MAP = {
|
|
109
|
-
button: [
|
|
110
|
-
form: [
|
|
111
|
-
table: [
|
|
112
|
-
chart: [
|
|
113
|
-
card: [
|
|
114
|
-
avatar: [
|
|
115
|
-
badge: [
|
|
116
|
-
alert: [
|
|
117
|
-
modal: [
|
|
118
|
-
drawer: [
|
|
119
|
-
tab: [
|
|
120
|
-
sidebar: [
|
|
121
|
-
navbar: [
|
|
122
|
-
breadcrumb: [
|
|
123
|
-
pagination: [
|
|
124
|
-
progress: [
|
|
125
|
-
slider: [
|
|
126
|
-
toggle: [
|
|
127
|
-
checkbox: [
|
|
128
|
-
radio: [
|
|
129
|
-
dropdown: [
|
|
130
|
-
select: [
|
|
131
|
-
upload: [
|
|
132
|
-
calendar: [
|
|
133
|
-
timeline: [
|
|
134
|
-
step: [
|
|
135
|
-
accordion: [
|
|
69
|
+
button: ["Button"],
|
|
70
|
+
form: ["FormContainer", "TextField", "Column"],
|
|
71
|
+
table: ["Table"],
|
|
72
|
+
chart: ["Chart"],
|
|
73
|
+
card: ["Card"],
|
|
74
|
+
avatar: ["Avatar"],
|
|
75
|
+
badge: ["Badge"],
|
|
76
|
+
alert: ["Alert"],
|
|
77
|
+
modal: ["Modal", "Dialog"],
|
|
78
|
+
drawer: ["Drawer"],
|
|
79
|
+
tab: ["Tabs", "Tab"],
|
|
80
|
+
sidebar: ["Sidebar"],
|
|
81
|
+
navbar: ["Nav"],
|
|
82
|
+
breadcrumb: ["Breadcrumb"],
|
|
83
|
+
pagination: ["Pagination"],
|
|
84
|
+
progress: ["Progress"],
|
|
85
|
+
slider: ["Slider"],
|
|
86
|
+
toggle: ["Toggle"],
|
|
87
|
+
checkbox: ["CheckBox"],
|
|
88
|
+
radio: ["Radio"],
|
|
89
|
+
dropdown: ["ChoicePicker", "Select"],
|
|
90
|
+
select: ["ChoicePicker", "Select"],
|
|
91
|
+
upload: ["Upload"],
|
|
92
|
+
calendar: ["CalendarPicker", "DateTimeInput"],
|
|
93
|
+
timeline: ["Timeline"],
|
|
94
|
+
step: ["Steps"],
|
|
95
|
+
accordion: ["Accordion"]
|
|
136
96
|
};
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Verify generated A2UI output against extracted expectations.
|
|
140
|
-
*
|
|
141
|
-
* @param {object[]} components — Flat adjacency array from messages[0].components
|
|
142
|
-
* @param {{ fields: string[], componentTypes: string[], quantities: { count: number, type: string }[], actions: string[], content: string[] }} expectations
|
|
143
|
-
* @returns {{ score: number, checks: { category: string, expected: string, found: boolean, detail: string }[], gaps: string[] }}
|
|
144
|
-
*/
|
|
145
|
-
export function verifyAlignment(components, expectations) {
|
|
97
|
+
function verifyAlignment(components, expectations) {
|
|
146
98
|
const checks = [];
|
|
147
99
|
const gaps = [];
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}).join(' ');
|
|
154
|
-
|
|
155
|
-
const allTypes = new Set(components.map(c => c.component));
|
|
156
|
-
|
|
157
|
-
// ── Check fields ──
|
|
100
|
+
const allText = components.map((c) => {
|
|
101
|
+
const texts = [c["textContent"], c["text"], c["label"], c["placeholder"], c["description"], c["name"]];
|
|
102
|
+
return texts.filter(Boolean).join(" ").toLowerCase();
|
|
103
|
+
}).join(" ");
|
|
104
|
+
const allTypes = new Set(components.map((c) => c.component));
|
|
158
105
|
for (const field of expectations.fields) {
|
|
159
|
-
const found = allText.includes(field) ||
|
|
160
|
-
components.some(c => (c.label || '').toLowerCase().includes(field) ||
|
|
161
|
-
(c.placeholder || '').toLowerCase().includes(field) ||
|
|
162
|
-
(c.name || '').toLowerCase().includes(field));
|
|
106
|
+
const found = allText.includes(field) || components.some((c) => (c["label"] ?? "").toLowerCase().includes(field) || (c["placeholder"] ?? "").toLowerCase().includes(field) || (c["name"] ?? "").toLowerCase().includes(field));
|
|
163
107
|
checks.push({
|
|
164
|
-
category:
|
|
108
|
+
category: "field",
|
|
165
109
|
expected: field,
|
|
166
110
|
found,
|
|
167
|
-
detail: found ? `Found "${field}" in component properties` : `Missing field: "${field}"
|
|
111
|
+
detail: found ? `Found "${field}" in component properties` : `Missing field: "${field}"`
|
|
168
112
|
});
|
|
169
113
|
if (!found) gaps.push(`Missing field "${field}"`);
|
|
170
114
|
}
|
|
171
|
-
|
|
172
|
-
// ── Check component types ──
|
|
173
115
|
for (const type of expectations.componentTypes) {
|
|
174
|
-
const mappedTypes = TYPE_MAP[type]
|
|
175
|
-
const found = mappedTypes.some(t => allTypes.has(t));
|
|
116
|
+
const mappedTypes = TYPE_MAP[type] ?? [type.charAt(0).toUpperCase() + type.slice(1)];
|
|
117
|
+
const found = mappedTypes.some((t) => allTypes.has(t));
|
|
176
118
|
checks.push({
|
|
177
|
-
category:
|
|
119
|
+
category: "componentType",
|
|
178
120
|
expected: type,
|
|
179
121
|
found,
|
|
180
|
-
detail: found ? `Found ${type} component` : `Missing component type: ${type}
|
|
122
|
+
detail: found ? `Found ${type} component` : `Missing component type: ${type}`
|
|
181
123
|
});
|
|
182
124
|
if (!found) gaps.push(`Missing ${type} component`);
|
|
183
125
|
}
|
|
184
|
-
|
|
185
|
-
// ── Check quantities ──
|
|
186
126
|
for (const { count, type } of expectations.quantities) {
|
|
187
|
-
const mappedTypes = TYPE_MAP[type]
|
|
188
|
-
const actual = components.filter(c => mappedTypes.some(t => c.component === t)).length;
|
|
127
|
+
const mappedTypes = TYPE_MAP[type] ?? [type.charAt(0).toUpperCase() + type.slice(1)];
|
|
128
|
+
const actual = components.filter((c) => mappedTypes.some((t) => c.component === t)).length;
|
|
189
129
|
const found = actual >= count;
|
|
190
130
|
checks.push({
|
|
191
|
-
category:
|
|
131
|
+
category: "quantity",
|
|
192
132
|
expected: `${count} ${type}(s)`,
|
|
193
133
|
found,
|
|
194
|
-
detail: found ? `Found ${actual} ${type}(s) (expected ${count})` : `Only ${actual} ${type}(s), expected ${count}
|
|
134
|
+
detail: found ? `Found ${actual} ${type}(s) (expected ${count})` : `Only ${actual} ${type}(s), expected ${count}`
|
|
195
135
|
});
|
|
196
136
|
if (!found) gaps.push(`Expected ${count} ${type}(s), found ${actual}`);
|
|
197
137
|
}
|
|
198
|
-
|
|
199
|
-
// ── Check actions ──
|
|
200
138
|
for (const action of expectations.actions) {
|
|
201
|
-
const found = allText.includes(action) ||
|
|
202
|
-
components.some(c => c.component === 'Button' && (c.text || '').toLowerCase().includes(action));
|
|
139
|
+
const found = allText.includes(action) || components.some((c) => c.component === "Button" && (c["text"] ?? "").toLowerCase().includes(action));
|
|
203
140
|
checks.push({
|
|
204
|
-
category:
|
|
141
|
+
category: "action",
|
|
205
142
|
expected: action,
|
|
206
143
|
found,
|
|
207
|
-
detail: found ? `Found "${action}" action` : `Missing action: "${action}"
|
|
144
|
+
detail: found ? `Found "${action}" action` : `Missing action: "${action}"`
|
|
208
145
|
});
|
|
209
146
|
if (!found) gaps.push(`Missing "${action}" action`);
|
|
210
147
|
}
|
|
211
|
-
|
|
212
|
-
// ── Check specific content ──
|
|
213
148
|
for (const text of expectations.content) {
|
|
214
149
|
const found = allText.includes(text.toLowerCase());
|
|
215
150
|
checks.push({
|
|
216
|
-
category:
|
|
151
|
+
category: "content",
|
|
217
152
|
expected: text,
|
|
218
153
|
found,
|
|
219
|
-
detail: found ? `Found content "${text}"` : `Missing content: "${text}"
|
|
154
|
+
detail: found ? `Found content "${text}"` : `Missing content: "${text}"`
|
|
220
155
|
});
|
|
221
156
|
if (!found) gaps.push(`Missing content "${text}"`);
|
|
222
157
|
}
|
|
223
|
-
|
|
224
|
-
// ── Score ──
|
|
225
158
|
const total = checks.length;
|
|
226
|
-
const passed = checks.filter(c => c.found).length;
|
|
227
|
-
const score = total > 0 ? Math.round(
|
|
228
|
-
|
|
159
|
+
const passed = checks.filter((c) => c.found).length;
|
|
160
|
+
const score = total > 0 ? Math.round(passed / total * 100) / 100 : 1;
|
|
229
161
|
return { score, checks, gaps };
|
|
230
162
|
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Full intent alignment check — extract + verify in one call.
|
|
234
|
-
*
|
|
235
|
-
* @param {string} intent
|
|
236
|
-
* @param {object[]} components
|
|
237
|
-
* @returns {{ score: number, checks: object[], gaps: string[], expectations: object }}
|
|
238
|
-
*/
|
|
239
|
-
export function checkIntentAlignment(intent, components) {
|
|
163
|
+
function checkIntentAlignment(intent, components) {
|
|
240
164
|
const expectations = extractExpectations(intent);
|
|
241
165
|
const result = verifyAlignment(components, expectations);
|
|
242
166
|
return { ...result, expectations };
|
|
243
167
|
}
|
|
168
|
+
export {
|
|
169
|
+
checkIntentAlignment,
|
|
170
|
+
extractExpectations,
|
|
171
|
+
verifyAlignment
|
|
172
|
+
};
|
|
@@ -72,7 +72,7 @@ export function extractExpectations(intent: string): Expectations {
|
|
|
72
72
|
pattern.lastIndex = 0;
|
|
73
73
|
let match;
|
|
74
74
|
while ((match = pattern.exec(lower))) {
|
|
75
|
-
const f = match[1].toLowerCase();
|
|
75
|
+
const f = (match[1] ?? '').toLowerCase();
|
|
76
76
|
if (!fields.includes(f)) fields.push(f);
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -83,7 +83,7 @@ export function extractExpectations(intent: string): Expectations {
|
|
|
83
83
|
pattern.lastIndex = 0;
|
|
84
84
|
let match;
|
|
85
85
|
while ((match = pattern.exec(lower))) {
|
|
86
|
-
const t = match[1].toLowerCase();
|
|
86
|
+
const t = (match[1] ?? '').toLowerCase();
|
|
87
87
|
if (!componentTypes.includes(t)) componentTypes.push(t);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
@@ -93,9 +93,9 @@ export function extractExpectations(intent: string): Expectations {
|
|
|
93
93
|
QUANTITY_PATTERN.lastIndex = 0;
|
|
94
94
|
let qMatch;
|
|
95
95
|
while ((qMatch = QUANTITY_PATTERN.exec(lower))) {
|
|
96
|
-
const raw = qMatch[1];
|
|
96
|
+
const raw = qMatch[1] ?? '';
|
|
97
97
|
const num = QUANTITY_MAP[raw] ?? parseInt(raw, 10);
|
|
98
|
-
const type = qMatch[2].replace(/s$/, ''); // singularize
|
|
98
|
+
const type = (qMatch[2] ?? '').replace(/s$/, ''); // singularize
|
|
99
99
|
if (!isNaN(num)) quantities.push({ count: num, type });
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -105,7 +105,7 @@ export function extractExpectations(intent: string): Expectations {
|
|
|
105
105
|
pattern.lastIndex = 0;
|
|
106
106
|
let match;
|
|
107
107
|
while ((match = pattern.exec(lower))) {
|
|
108
|
-
const a = match[1].toLowerCase();
|
|
108
|
+
const a = (match[1] ?? '').toLowerCase();
|
|
109
109
|
if (!actions.includes(a)) actions.push(a);
|
|
110
110
|
}
|
|
111
111
|
}
|
|
@@ -1,97 +1,69 @@
|
|
|
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
1
|
const CATEGORY_RULES = [
|
|
9
2
|
// Form categories
|
|
10
|
-
{ category:
|
|
11
|
-
{ category:
|
|
12
|
-
{ category:
|
|
13
|
-
{ category:
|
|
14
|
-
{ category:
|
|
15
|
-
|
|
3
|
+
{ category: "form/login", keywords: ["login", "sign in", "signin", "log in", "authentication"] },
|
|
4
|
+
{ category: "form/signup", keywords: ["signup", "sign up", "register", "registration", "create account", "onboarding"] },
|
|
5
|
+
{ category: "form/contact", keywords: ["contact", "contact us", "reach out", "get in touch", "enquiry", "inquiry"] },
|
|
6
|
+
{ category: "form/settings", keywords: ["settings", "preferences", "config", "configuration", "account settings", "profile edit"] },
|
|
7
|
+
{ category: "form/checkout", keywords: ["checkout", "check out", "payment", "billing", "purchase"] },
|
|
16
8
|
// Data categories
|
|
17
|
-
{ category:
|
|
18
|
-
{ category:
|
|
19
|
-
{ category:
|
|
20
|
-
|
|
9
|
+
{ category: "data/table", keywords: ["table", "data table", "spreadsheet", "grid view", "list view", "datagrid"] },
|
|
10
|
+
{ category: "data/dashboard", keywords: ["dashboard", "kpi", "metrics", "analytics", "overview", "summary panel", "stats"] },
|
|
11
|
+
{ category: "data/chart", keywords: ["chart", "graph", "visualization", "pie chart", "bar chart", "line chart", "histogram"] },
|
|
21
12
|
// Layout categories
|
|
22
|
-
{ category:
|
|
23
|
-
{ category:
|
|
24
|
-
{ category:
|
|
25
|
-
|
|
13
|
+
{ category: "layout/landing", keywords: ["landing", "landing page", "homepage", "hero", "splash"] },
|
|
14
|
+
{ category: "layout/profile", keywords: ["profile", "user profile", "avatar", "bio", "about me"] },
|
|
15
|
+
{ category: "layout/pricing", keywords: ["pricing", "pricing table", "plans", "subscription", "tier"] },
|
|
26
16
|
// Navigation categories
|
|
27
|
-
{ category:
|
|
28
|
-
{ category:
|
|
29
|
-
|
|
17
|
+
{ category: "nav/sidebar", keywords: ["sidebar", "side nav", "navigation menu", "drawer", "side panel"] },
|
|
18
|
+
{ category: "nav/tabs", keywords: ["tabs", "tab bar", "tabbed", "tab navigation", "tab panel"] },
|
|
30
19
|
// Agent categories
|
|
31
|
-
{ category:
|
|
32
|
-
{ category:
|
|
33
|
-
|
|
20
|
+
{ category: "agent/chat", keywords: ["chat", "chatbot", "messenger", "conversation", "messaging", "chat interface"] },
|
|
21
|
+
{ category: "agent/notification", keywords: ["notification", "alert", "toast", "snackbar", "banner", "announcement"] },
|
|
34
22
|
// Content categories
|
|
35
|
-
{ category:
|
|
36
|
-
{ category:
|
|
37
|
-
|
|
23
|
+
{ category: "content/blog", keywords: ["blog", "article", "post", "news", "editorial", "content feed"] },
|
|
24
|
+
{ category: "content/faq", keywords: ["faq", "frequently asked", "questions", "help center", "knowledge base", "accordion"] },
|
|
38
25
|
// Commerce categories
|
|
39
|
-
{ category:
|
|
40
|
-
{ category:
|
|
41
|
-
{ category:
|
|
42
|
-
|
|
26
|
+
{ category: "commerce/product", keywords: ["product", "product card", "product page", "product detail", "item detail", "catalog"] },
|
|
27
|
+
{ category: "commerce/cart", keywords: ["cart", "shopping cart", "basket", "bag"] },
|
|
28
|
+
{ category: "commerce/order", keywords: ["order", "order history", "order summary", "receipt", "invoice", "order tracking"] },
|
|
43
29
|
// Workflow categories
|
|
44
|
-
{ category:
|
|
45
|
-
{ category:
|
|
46
|
-
|
|
30
|
+
{ category: "workflow/wizard", keywords: ["wizard", "stepper", "multi-step", "step form", "onboard flow", "setup wizard"] },
|
|
31
|
+
{ category: "workflow/kanban", keywords: ["kanban", "board", "task board", "trello", "project board", "drag and drop"] },
|
|
47
32
|
// Status categories
|
|
48
|
-
{ category:
|
|
49
|
-
{ category:
|
|
50
|
-
{ category:
|
|
33
|
+
{ category: "status/error", keywords: ["error", "error page", "404", "500", "not found", "something went wrong"] },
|
|
34
|
+
{ category: "status/empty", keywords: ["empty state", "no results", "no data", "zero state", "blank slate"] },
|
|
35
|
+
{ category: "status/loading", keywords: ["loading", "skeleton", "spinner", "progress", "placeholder"] }
|
|
51
36
|
];
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
* @param {string} intent — Free-text intent string
|
|
57
|
-
* @returns {{ category: string, confidence: number }}
|
|
58
|
-
*/
|
|
59
|
-
export function categorizeIntent(intent) {
|
|
60
|
-
if (!intent || typeof intent !== 'string') {
|
|
61
|
-
return { category: 'other', confidence: 0 };
|
|
37
|
+
function categorizeIntent(intent) {
|
|
38
|
+
if (!intent || typeof intent !== "string") {
|
|
39
|
+
return { category: "other", confidence: 0 };
|
|
62
40
|
}
|
|
63
|
-
|
|
64
41
|
const lower = intent.toLowerCase().trim();
|
|
65
|
-
let bestCategory =
|
|
42
|
+
let bestCategory = "other";
|
|
66
43
|
let bestScore = 0;
|
|
67
|
-
|
|
68
44
|
for (const rule of CATEGORY_RULES) {
|
|
69
45
|
let matchCount = 0;
|
|
70
46
|
let longestMatch = 0;
|
|
71
|
-
|
|
72
47
|
for (const kw of rule.keywords) {
|
|
73
48
|
if (lower.includes(kw)) {
|
|
74
49
|
matchCount++;
|
|
75
50
|
longestMatch = Math.max(longestMatch, kw.length);
|
|
76
51
|
}
|
|
77
52
|
}
|
|
78
|
-
|
|
79
53
|
if (matchCount === 0) continue;
|
|
80
|
-
|
|
81
|
-
// Score: multi-word keyword matches get higher confidence,
|
|
82
|
-
// more matches within a category = higher confidence
|
|
83
|
-
const kwLenBonus = longestMatch / lower.length; // longer keyword relative to intent = more specific
|
|
54
|
+
const kwLenBonus = longestMatch / lower.length;
|
|
84
55
|
const multiMatchBonus = Math.min(matchCount * 0.15, 0.3);
|
|
85
56
|
const score = 0.5 + kwLenBonus * 0.3 + multiMatchBonus;
|
|
86
|
-
|
|
87
57
|
if (score > bestScore) {
|
|
88
58
|
bestScore = score;
|
|
89
59
|
bestCategory = rule.category;
|
|
90
60
|
}
|
|
91
61
|
}
|
|
92
|
-
|
|
93
62
|
return {
|
|
94
63
|
category: bestCategory,
|
|
95
|
-
confidence: Math.min(Math.round(bestScore * 100) / 100, 1)
|
|
64
|
+
confidence: Math.min(Math.round(bestScore * 100) / 100, 1)
|
|
96
65
|
};
|
|
97
66
|
}
|
|
67
|
+
export {
|
|
68
|
+
categorizeIntent
|
|
69
|
+
};
|