@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/domain-router.js +362 -117
  3. package/embedding/chunk-embedding-retriever.js +47 -79
  4. package/embedding/embedding-provider.js +35 -71
  5. package/embedding/index.js +2 -10
  6. package/feedback/dialog-recorder.js +61 -145
  7. package/feedback/feedback-analyzer.js +46 -102
  8. package/feedback/feedback-store.js +91 -107
  9. package/feedback/feedback.js +36 -117
  10. package/feedback/gap-registry.js +40 -82
  11. package/feedback/index.js +14 -12
  12. package/index.d.ts +4 -0
  13. package/index.js +53 -16
  14. package/intent/clarity.js +61 -129
  15. package/intent/decomposer.js +51 -143
  16. package/intent/index.js +18 -14
  17. package/intent/intent-alignment.js +79 -150
  18. package/intent/intent-categorizer.js +34 -62
  19. package/intent/intent-gate.js +43 -102
  20. package/intent/prompt-analyzer.js +68 -126
  21. package/package.json +4 -2
  22. package/wiring-catalog.js +95 -146
  23. package/embedding/chunk-embedding-retriever.ts +0 -156
  24. package/embedding/embedding-provider.ts +0 -111
  25. package/embedding/index.ts +0 -10
  26. package/feedback/dialog-recorder.ts +0 -172
  27. package/feedback/feedback-analyzer.ts +0 -250
  28. package/feedback/feedback-store.ts +0 -229
  29. package/feedback/feedback.ts +0 -201
  30. package/feedback/gap-registry.ts +0 -137
  31. package/feedback/index.ts +0 -14
  32. package/intent/clarity.ts +0 -224
  33. package/intent/decomposer.ts +0 -229
  34. package/intent/index.ts +0 -20
  35. package/intent/intent-alignment.ts +0 -267
  36. package/intent/intent-categorizer.ts +0 -104
  37. package/intent/intent-gate.ts +0 -151
  38. package/intent/prompt-analyzer.ts +0 -231
@@ -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
- }