@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Changelog — @adia-ai/a2ui-retrieval
|
|
2
|
+
|
|
3
|
+
Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
|
4
|
+
[Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
_Nothing yet._
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## [0.0.1] - 2026-04-24
|
|
13
|
+
|
|
14
|
+
First public release. Extracted from
|
|
15
|
+
[`@adia-ai/a2ui-compose`](../compose/) during the 2026-04-24
|
|
16
|
+
consolidation so non-compose tooling (tests, MCP validator tools,
|
|
17
|
+
CI gates) can depend on retrieval without pulling the full
|
|
18
|
+
generator graph.
|
|
19
|
+
|
|
20
|
+
### Included
|
|
21
|
+
|
|
22
|
+
- **Pattern library** (`pattern-library.js`) — indexed search over the
|
|
23
|
+
catalog's patterns + fragments; domain-aware ranking; loads patterns
|
|
24
|
+
from `@adia-ai/a2ui-corpus` at runtime.
|
|
25
|
+
- **Intent gate** (`intent-gate.js`) — classifies UI-generating vs
|
|
26
|
+
conversational intents based on domain-keyword signal. Zero-signal
|
|
27
|
+
defaults to conversational (prevents hallucinated UI from
|
|
28
|
+
motivational / abstract text).
|
|
29
|
+
- **Domain router** (`domain-router.js`) — free-form text → catalog
|
|
30
|
+
domain (`forms`, `data`, `agent`, `navigation`, `layout`, etc.).
|
|
31
|
+
- **Anti-pattern detector** (`anti-patterns.js`) — flags invented
|
|
32
|
+
components + bad attribute combos against the registry.
|
|
33
|
+
- **Clarity analyzer** (`clarity.js`) — scores intent specificity;
|
|
34
|
+
drives clarification prompts when signal is too weak.
|
|
35
|
+
- **Context assembly** (`context.js`) — stitches retrieval results
|
|
36
|
+
into prompt context for LLM adapters.
|
|
37
|
+
- **Embedding retriever** (`embedding-retriever.js`) — dense-vector
|
|
38
|
+
pattern retrieval (optional; gated on the presence of embeddings
|
|
39
|
+
in `@adia-ai/a2ui-corpus`).
|
|
40
|
+
- **Prompt analyzer** (`prompt-analyzer.js`) — prompt-side features
|
|
41
|
+
feeding domain + clarity scoring.
|
|
42
|
+
- **Dialog recorder** (`dialog-recorder.js`) — opt-in JSONL capture
|
|
43
|
+
of LLM round-trips for offline replay + tuning (gated on
|
|
44
|
+
`ADIA_LOG_DIALOGS=1`).
|
|
45
|
+
- **Synthetic data** (`synthetic-data.js`) — deterministic
|
|
46
|
+
pattern-match-only generator used by the `instant` engine mode.
|
|
47
|
+
|
|
48
|
+
### Dependencies
|
|
49
|
+
|
|
50
|
+
- `@adia-ai/a2ui-utils` — A2UI registry + runtime primitives.
|
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# `@adia-ai/a2ui-retrieval`
|
|
2
|
+
|
|
3
|
+
Retrieval layer for the A2UI (Agent-to-UI) protocol — catalog lookup,
|
|
4
|
+
intent classification, domain routing, pattern + anti-pattern matching,
|
|
5
|
+
clarity + context assembly. Consumed by
|
|
6
|
+
[`@adia-ai/a2ui-compose`](../compose/) and any A2UI-protocol tooling
|
|
7
|
+
that needs to reason about user intent against the catalog.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @adia-ai/a2ui-retrieval
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## What's here
|
|
16
|
+
|
|
17
|
+
- **Pattern library** — indexed search over the catalog's patterns and
|
|
18
|
+
fragments; domain-aware ranking.
|
|
19
|
+
- **Intent gate** — classifies a user intent as UI-generating vs
|
|
20
|
+
conversational based on domain-keyword signal.
|
|
21
|
+
- **Domain router** — maps free-form text to catalog domains
|
|
22
|
+
(`forms`, `data`, `agent`, `navigation`, etc.).
|
|
23
|
+
- **Anti-pattern detector** — flags compositions that violate invariants
|
|
24
|
+
(invented components, bad attribute combos).
|
|
25
|
+
- **Clarity analyzer** — scores how specific an intent is; drives
|
|
26
|
+
clarification prompts when the signal is too weak to generate.
|
|
27
|
+
- **Context assembly** — stitches retrieval results into the prompt
|
|
28
|
+
context consumed by the LLM adapters.
|
|
29
|
+
|
|
30
|
+
## Runtime
|
|
31
|
+
|
|
32
|
+
Framework-agnostic JS. Depends only on
|
|
33
|
+
[`@adia-ai/a2ui-utils`](https://www.npmjs.com/package/@adia-ai/a2ui-utils)
|
|
34
|
+
for the A2UI registry + runtime primitives.
|
|
35
|
+
|
|
36
|
+
## Links
|
|
37
|
+
|
|
38
|
+
- Repo: [`adiahealth/gen-ui-kit`](https://github.com/adiahealth/gen-ui-kit)
|
|
39
|
+
- Architecture: [`docs/specs/package-architecture.md`](https://github.com/adiahealth/gen-ui-kit/blob/main/docs/specs/package-architecture.md)
|
|
40
|
+
- CHANGELOG: [`CHANGELOG.md`](./CHANGELOG.md)
|
package/anti-patterns.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anti-Pattern Registry — known bad patterns from AdiaUI best practices.
|
|
3
|
+
*
|
|
4
|
+
* Each anti-pattern has:
|
|
5
|
+
* - name: machine identifier
|
|
6
|
+
* - description: what the anti-pattern is
|
|
7
|
+
* - check: regex or function to detect it in HTML strings
|
|
8
|
+
* - fix: guidance on how to correct it
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const antiPatterns = [
|
|
12
|
+
{
|
|
13
|
+
name: 'noBareDivs',
|
|
14
|
+
description: 'All elements should be AdiaUI components, not bare <div> elements.',
|
|
15
|
+
check: /<div[\s>]/i,
|
|
16
|
+
fix: 'Replace <div> with the appropriate AdiaUI layout component: Row, Column, Grid, Card, or Section.',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'noBareInputs',
|
|
20
|
+
description: 'Form controls should use component wrappers, not native <input>/<select>/<textarea>.',
|
|
21
|
+
check: /<(input|select|textarea)[\s>]/i,
|
|
22
|
+
fix: 'Use input-ui, select-ui, textarea-ui, check-ui, switch-ui, slider-ui, or datetime-ui instead.',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'cardStructure',
|
|
26
|
+
description: 'Card children must follow the header/section/footer anatomy. Header and footer are direct children of card.',
|
|
27
|
+
check: (html) => {
|
|
28
|
+
// Detect section > header or section > footer (wrong nesting)
|
|
29
|
+
return /<section[^>]*>\s*<header/i.test(html) || /<section[^>]*>\s*<footer/i.test(html);
|
|
30
|
+
},
|
|
31
|
+
fix: 'Header and Footer must be direct children of Card, not nested inside Section. Structure: Card > Header + Section + Footer.',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'flatAdjacency',
|
|
35
|
+
description: 'AdiaUI uses flat component lists with ID references, not deeply nested component trees.',
|
|
36
|
+
check: (html) => {
|
|
37
|
+
// Heuristic: detect 5+ levels of nesting with AdiaUI components
|
|
38
|
+
const nested = html.match(/<[a-z]+-(?:n|ui)[^>]*>\s*<[a-z]+-(?:n|ui)[^>]*>\s*<[a-z]+-(?:n|ui)[^>]*>\s*<[a-z]+-(?:n|ui)[^>]*>\s*<[a-z]+-(?:n|ui)/i);
|
|
39
|
+
return !!nested;
|
|
40
|
+
},
|
|
41
|
+
fix: 'Use a flat adjacency list with unique IDs and children arrays referencing IDs.',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'columnWrap',
|
|
45
|
+
description: 'Content inside Section must be wrapped in a Column (col-ui), not placed directly.',
|
|
46
|
+
check: (html) => {
|
|
47
|
+
// Detect section > text-ui or section > p (without col-ui wrapper)
|
|
48
|
+
return /<section[^>]*>\s*<(text-ui|p |p>|h[1-6])/i.test(html);
|
|
49
|
+
},
|
|
50
|
+
fix: 'Wrap section content in <col-ui>: Section > Column > Text/content.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'noHardcodedColors',
|
|
54
|
+
description: 'Use design token colors (--a-*) instead of hardcoded hex/rgb/hsl values.',
|
|
55
|
+
check: /(style\s*=\s*"[^"]*(?:color|background)\s*:\s*(?:#[0-9a-f]{3,8}|rgb|hsl))/i,
|
|
56
|
+
fix: 'Use token-based colors via variant attributes or CSS custom properties (--a-canvas-*, --a-accent-*, etc.).',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'noInlineLayout',
|
|
60
|
+
description: 'Use declarative layout attributes instead of inline display/flex/grid styles.',
|
|
61
|
+
check: /(style\s*=\s*"[^"]*(?:display\s*:|flex|grid-template))/i,
|
|
62
|
+
fix: 'Use Row, Column, Grid components with gap/columns attributes instead of inline CSS layout.',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'noInventedComponents',
|
|
66
|
+
description: 'Only use components from the AdiaUI registry. Do not invent custom component tags.',
|
|
67
|
+
check: (html) => {
|
|
68
|
+
const tags = html.match(/<([a-z]+-(?:n|ui))\b/gi) || [];
|
|
69
|
+
const known = new Set([
|
|
70
|
+
'row-ui', 'col-ui', 'list-ui', 'grid-ui', 'text-ui', 'image-ui',
|
|
71
|
+
'icon-ui', 'divider-ui', 'badge-ui', 'avatar-ui', 'progress-ui',
|
|
72
|
+
'skeleton-ui', 'input-ui', 'input-ui', 'check-ui', 'switch-ui',
|
|
73
|
+
'slider-ui', 'select-ui', 'datetime-ui', 'search-ui', 'upload-ui',
|
|
74
|
+
'button-ui', 'card-ui', 'tabs-ui', 'tab-ui', 'pane-ui', 'modal-ui',
|
|
75
|
+
'drawer-ui', 'toast-ui', 'popover-ui', 'stream-ui', 'form-container-ui',
|
|
76
|
+
'table-ui', 'chart-ui', 'embed-ui', 'stack-ui', 'block-ui', 'code-ui',
|
|
77
|
+
'textarea-ui', 'radio-ui', 'radio-ui', 'tag-ui', 'accordion-ui',
|
|
78
|
+
'modal-ui', 'alert-ui', 'tooltip-ui', 'menu-ui', 'breadcrumb-ui',
|
|
79
|
+
'nav-n', 'pagination-ui', 'avatar-group-ui', 'segmented-ui',
|
|
80
|
+
'segment-ui', 'command-ui', 'command-item-ui', 'command-group-ui',
|
|
81
|
+
'calendar-picker-ui', 'color-picker-ui', 'kbd-ui', 'toolbar-ui',
|
|
82
|
+
'otp-input-ui',
|
|
83
|
+
'theme-ui', 'router-ui', 'adia-root', 'noodles-ui',
|
|
84
|
+
'listbox-ui', 'icon-provider',
|
|
85
|
+
]);
|
|
86
|
+
for (const match of tags) {
|
|
87
|
+
const tag = match.slice(1).toLowerCase();
|
|
88
|
+
if (!known.has(tag)) return true;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
},
|
|
92
|
+
fix: 'Only use registered AdiaUI types. Check the registry for available components.',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'slotOnContainer',
|
|
96
|
+
description: 'Slot attributes (slot="heading", slot="description", slot="action") must go on the content children inside Header/Footer, not on Header/Footer elements themselves.',
|
|
97
|
+
check: (html) => {
|
|
98
|
+
return /<header[^>]*\bslot\s*=/i.test(html) || /<footer[^>]*\bslot\s*=/i.test(html);
|
|
99
|
+
},
|
|
100
|
+
fix: 'Move slot="heading" from <header> to the Text/heading child inside it. Header is a grid container — slots go on its children.',
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get all registered anti-patterns.
|
|
106
|
+
* @returns {Array<{name: string, description: string, fix: string}>}
|
|
107
|
+
*/
|
|
108
|
+
export function getAntiPatterns() {
|
|
109
|
+
return antiPatterns.map(({ name, description, fix }) => ({ name, description, fix }));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check an HTML string against a specific anti-pattern.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} name — Anti-pattern name
|
|
116
|
+
* @param {string} html — HTML string to check
|
|
117
|
+
* @returns {{ violated: boolean, name: string, description: string, fix: string } | null}
|
|
118
|
+
* Returns the violation object if detected, null if the pattern name is unknown.
|
|
119
|
+
*/
|
|
120
|
+
export function checkAntiPattern(name, html) {
|
|
121
|
+
const pattern = antiPatterns.find(p => p.name === name);
|
|
122
|
+
if (!pattern) return null;
|
|
123
|
+
|
|
124
|
+
const violated = typeof pattern.check === 'function'
|
|
125
|
+
? pattern.check(html)
|
|
126
|
+
: pattern.check.test(html);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
violated,
|
|
130
|
+
name: pattern.name,
|
|
131
|
+
description: pattern.description,
|
|
132
|
+
fix: pattern.fix,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check HTML against all anti-patterns. Returns only violations.
|
|
138
|
+
*
|
|
139
|
+
* @param {string} html — HTML string to check
|
|
140
|
+
* @returns {Array<{name: string, description: string, fix: string}>}
|
|
141
|
+
*/
|
|
142
|
+
export function checkAllAntiPatterns(html) {
|
|
143
|
+
return antiPatterns
|
|
144
|
+
.filter(p => {
|
|
145
|
+
return typeof p.check === 'function' ? p.check(html) : p.check.test(html);
|
|
146
|
+
})
|
|
147
|
+
.map(({ name, description, fix }) => ({ name, description, fix }));
|
|
148
|
+
}
|
package/catalog.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CatalogManifest — Structured index of all AdiaUI components.
|
|
3
|
+
*
|
|
4
|
+
* Reads from the per-component a2ui.json sidecars co-located with each
|
|
5
|
+
* component (packages/web-components/components/<c>/<c>.a2ui.json), generated
|
|
6
|
+
* from the YAML single-source-of-truth by scripts/build-components.mjs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { registry } from '@adia-ai/a2ui-utils';
|
|
10
|
+
|
|
11
|
+
// ── Schema loading ──
|
|
12
|
+
const schemaByTag = new Map();
|
|
13
|
+
let schemasLoaded = false;
|
|
14
|
+
|
|
15
|
+
function tagFromSchema(schema) {
|
|
16
|
+
return schema?.['x-adiaui']?.tag || null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function loadSchemas() {
|
|
20
|
+
if (schemasLoaded) return;
|
|
21
|
+
schemasLoaded = true;
|
|
22
|
+
|
|
23
|
+
if (typeof process !== 'undefined' && process.versions?.node) {
|
|
24
|
+
try {
|
|
25
|
+
const { readdir, readFile, stat } = await import(/* @vite-ignore */ 'node:fs/promises');
|
|
26
|
+
const { fileURLToPath } = await import(/* @vite-ignore */ 'node:url');
|
|
27
|
+
const { dirname, join } = await import(/* @vite-ignore */ 'node:path');
|
|
28
|
+
const compsDir = join(dirname(fileURLToPath(import.meta.url)), '../../web-components/components');
|
|
29
|
+
const entries = await readdir(compsDir, { withFileTypes: true });
|
|
30
|
+
for (const e of entries) {
|
|
31
|
+
if (!e.isDirectory()) continue;
|
|
32
|
+
const path = join(compsDir, e.name, `${e.name}.a2ui.json`);
|
|
33
|
+
try {
|
|
34
|
+
const raw = await readFile(path, 'utf8');
|
|
35
|
+
const schema = JSON.parse(raw);
|
|
36
|
+
const tag = tagFromSchema(schema);
|
|
37
|
+
if (tag) schemaByTag.set(tag, schema);
|
|
38
|
+
} catch { /* skip missing / malformed */ }
|
|
39
|
+
}
|
|
40
|
+
} catch { /* components dir missing */ }
|
|
41
|
+
} else {
|
|
42
|
+
try {
|
|
43
|
+
const modules = import.meta.glob('../../web-components/components/*/*.a2ui.json', { query: '?raw', import: 'default' });
|
|
44
|
+
for (const [, loader] of Object.entries(modules)) {
|
|
45
|
+
try {
|
|
46
|
+
const raw = await loader();
|
|
47
|
+
const schema = JSON.parse(raw);
|
|
48
|
+
const tag = tagFromSchema(schema);
|
|
49
|
+
if (tag) schemaByTag.set(tag, schema);
|
|
50
|
+
} catch { /* skip invalid */ }
|
|
51
|
+
}
|
|
52
|
+
} catch { /* glob not available */ }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Register a JSON schema for a component tag (runtime).
|
|
58
|
+
*/
|
|
59
|
+
export function registerSchema(tag, schema) {
|
|
60
|
+
schemaByTag.set(tag, schema);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Category classification ──
|
|
64
|
+
const categoryMap = {
|
|
65
|
+
'row-ui': 'layout', 'col-ui': 'layout', 'list-ui': 'layout', 'grid-ui': 'layout',
|
|
66
|
+
'stack-ui': 'layout', 'block-ui': 'layout', 'toolbar-ui': 'layout',
|
|
67
|
+
'text-ui': 'display', 'image-ui': 'display', 'icon-ui': 'display',
|
|
68
|
+
'divider-ui': 'display', 'badge-ui': 'display', 'avatar-ui': 'display',
|
|
69
|
+
'avatar-group-ui': 'display', 'progress-ui': 'display', 'skeleton-ui': 'display',
|
|
70
|
+
'code-ui': 'display', 'kbd-ui': 'display', 'tag-ui': 'display',
|
|
71
|
+
'stat-ui': 'display', 'empty-state-ui': 'display',
|
|
72
|
+
'input-ui': 'input', 'check-ui': 'input', 'switch-ui': 'input',
|
|
73
|
+
'slider-ui': 'input', 'select-ui': 'input', 'search-ui': 'input',
|
|
74
|
+
'upload-ui': 'input', 'textarea-ui': 'input', 'radio-ui': 'input',
|
|
75
|
+
'otp-input-ui': 'input', 'calendar-picker-ui': 'input', 'color-picker-ui': 'input',
|
|
76
|
+
'range-ui': 'input',
|
|
77
|
+
'button-ui': 'action',
|
|
78
|
+
'card-ui': 'container', 'tabs-ui': 'container', 'tab-ui': 'container',
|
|
79
|
+
'pane-ui': 'container', 'modal-ui': 'container', 'drawer-ui': 'container',
|
|
80
|
+
'toast-ui': 'container', 'popover-ui': 'container', 'accordion-ui': 'container',
|
|
81
|
+
'alert-ui': 'container', 'tooltip-ui': 'container', 'menu-ui': 'container',
|
|
82
|
+
'command-ui': 'container',
|
|
83
|
+
'section': 'card-child', 'header': 'card-child', 'footer': 'card-child',
|
|
84
|
+
'stream-ui': 'agent', 'table-ui': 'agent', 'chart-ui': 'agent',
|
|
85
|
+
'embed-ui': 'agent', 'noodles-ui': 'agent',
|
|
86
|
+
'breadcrumb-ui': 'navigation', 'nav-n': 'navigation', 'pagination-ui': 'navigation',
|
|
87
|
+
'segmented-ui': 'navigation', 'segment-ui': 'navigation', 'router-ui': 'navigation',
|
|
88
|
+
'toggle-group-ui': 'navigation', 'toggle-option-ui': 'navigation',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// ── Trait registry ──
|
|
92
|
+
const traitRegistry = [
|
|
93
|
+
{ name: 'pressable', category: 'input-interaction', description: 'Normalizes click/tap/keyboard into a single "press" event' },
|
|
94
|
+
{ name: 'focusable', category: 'input-interaction', description: 'Keyboard-only focus ring, ignores pointer focus' },
|
|
95
|
+
{ name: 'hoverable', category: 'input-interaction', description: 'Pointer hover enter/leave state tracking' },
|
|
96
|
+
{ name: 'active-state', category: 'input-interaction', description: 'Tracks pointer down / active interaction state' },
|
|
97
|
+
{ name: 'disabled-state', category: 'input-interaction', description: 'Enforces disabled behavior and interaction blocking' },
|
|
98
|
+
{ name: 'keyboard-nav', category: 'keyboard-navigation', description: 'Arrow keys, Enter, Escape — semantic navigation events' },
|
|
99
|
+
{ name: 'roving-tabindex', category: 'keyboard-navigation', description: 'Focus management within composite widgets' },
|
|
100
|
+
{ name: 'typeahead', category: 'keyboard-navigation', description: 'Incremental search within a collection' },
|
|
101
|
+
{ name: 'hotkey', category: 'keyboard-navigation', description: 'Global or scoped keyboard shortcuts' },
|
|
102
|
+
{ name: 'focus-trap', category: 'keyboard-navigation', description: 'Traps Tab/Shift+Tab within a container' },
|
|
103
|
+
{ name: 'form-associated', category: 'forms-data', description: 'ElementInternals integration for form participation' },
|
|
104
|
+
{ name: 'validation', category: 'forms-data', description: 'Validation rules: required, minlength, pattern, email' },
|
|
105
|
+
{ name: 'value-sync', category: 'forms-data', description: 'Syncs value between attribute/property/controller' },
|
|
106
|
+
{ name: 'dirty-state', category: 'forms-data', description: 'Tracks modified vs initial value' },
|
|
107
|
+
{ name: 'resettable', category: 'forms-data', description: 'Responds to form reset events' },
|
|
108
|
+
{ name: 'resize-observer-trait', category: 'layout-measurement', description: 'Reacts to element size changes' },
|
|
109
|
+
{ name: 'intersection-observer-trait', category: 'layout-measurement', description: 'Visibility detection (viewport)' },
|
|
110
|
+
{ name: 'anchor-positioning', category: 'layout-measurement', description: 'Positions relative to an anchor element' },
|
|
111
|
+
{ name: 'portal', category: 'layout-measurement', description: 'Renders content in a different DOM root' },
|
|
112
|
+
{ name: 'scroll-lock', category: 'layout-measurement', description: 'Disables background scrolling' },
|
|
113
|
+
{ name: 'draggable', category: 'motion-positioning', description: 'Pointer drag to reposition' },
|
|
114
|
+
{ name: 'tossable', category: 'motion-positioning', description: 'Flick with momentum + viewport bounce' },
|
|
115
|
+
{ name: 'resizable', category: 'motion-positioning', description: 'Drag edges/corners to resize' },
|
|
116
|
+
{ name: 'inertia-drag', category: 'motion-positioning', description: 'Momentum-based dragging, smooth deceleration' },
|
|
117
|
+
{ name: 'snap-to-grid', category: 'motion-positioning', description: 'Snaps position to configurable grid' },
|
|
118
|
+
{ name: 'drag-ghost', category: 'motion-positioning', description: 'Ghost clone at origin during drag' },
|
|
119
|
+
{ name: 'ripple', category: 'animation-feedback', description: 'Material-style press ripple effect' },
|
|
120
|
+
{ name: 'spring-animate', category: 'animation-feedback', description: 'Spring-based motion transitions' },
|
|
121
|
+
{ name: 'fade-presence', category: 'animation-feedback', description: 'Enter/exit fade with lifecycle' },
|
|
122
|
+
{ name: 'scale-press', category: 'animation-feedback', description: 'Subtle scale transform on press' },
|
|
123
|
+
{ name: 'tilt-hover', category: 'animation-feedback', description: 'Tilt based on pointer position' },
|
|
124
|
+
{ name: 'glow-focus', category: 'visual-dynamics', description: 'Animated pulsing glow on focus' },
|
|
125
|
+
{ name: 'gradient-shift', category: 'visual-dynamics', description: 'Animated rainbow gradient backgrounds' },
|
|
126
|
+
{ name: 'parallax', category: 'visual-dynamics', description: 'Layered motion relative to pointer' },
|
|
127
|
+
{ name: 'shimmer-loading', category: 'visual-dynamics', description: 'Skeleton shimmer effect' },
|
|
128
|
+
{ name: 'noise-texture', category: 'visual-dynamics', description: 'Procedural grain overlay' },
|
|
129
|
+
{ name: 'magnetic-hover', category: 'interaction-delight', description: 'Element subtly follows cursor' },
|
|
130
|
+
{ name: 'confetti', category: 'interaction-delight', description: 'Radial particle burst on press' },
|
|
131
|
+
{ name: 'confetti-burst', category: 'interaction-delight', description: 'Upward fountain particle burst' },
|
|
132
|
+
{ name: 'sound-feedback', category: 'audio-haptics-sensory', description: 'Synthesized tones via Web Audio API' },
|
|
133
|
+
{ name: 'haptic-feedback', category: 'audio-haptics-sensory', description: 'Vibration API feedback' },
|
|
134
|
+
{ name: 'typewriter', category: 'audio-haptics-sensory', description: 'Animated text reveal character by character' },
|
|
135
|
+
{ name: 'count-up', category: 'audio-haptics-sensory', description: 'Animated numeric transitions' },
|
|
136
|
+
{ name: 'attention-pulse', category: 'audio-haptics-sensory', description: 'Periodic pulse to draw attention' },
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
// ── Alias tracking ──
|
|
140
|
+
const aliases = new Set([
|
|
141
|
+
'Select', 'LoadingIndicator', 'ErrorContainer',
|
|
142
|
+
'Keyboard', 'DatePicker', 'CommandPalette', 'Segmented', 'OTP', 'SideNav', 'IconSource',
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
// ── Build the catalog ──
|
|
146
|
+
let catalog = null;
|
|
147
|
+
|
|
148
|
+
async function buildCatalog() {
|
|
149
|
+
if (catalog) return catalog;
|
|
150
|
+
await loadSchemas();
|
|
151
|
+
|
|
152
|
+
const entries = new Map();
|
|
153
|
+
|
|
154
|
+
for (const [type, tag] of registry) {
|
|
155
|
+
if (aliases.has(type)) continue;
|
|
156
|
+
// Skip -ui alias entries (lowercase with dash)
|
|
157
|
+
if (type.includes('-')) continue;
|
|
158
|
+
|
|
159
|
+
const schema = schemaByTag.get(tag) || null;
|
|
160
|
+
const category = categoryMap[tag] || 'other';
|
|
161
|
+
|
|
162
|
+
entries.set(type, {
|
|
163
|
+
type,
|
|
164
|
+
tag,
|
|
165
|
+
category,
|
|
166
|
+
description: schema?.description || '',
|
|
167
|
+
schema,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
catalog = {
|
|
172
|
+
version: '0.1.0',
|
|
173
|
+
totalTypes: entries.size,
|
|
174
|
+
entries,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return catalog;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function getCatalog() {
|
|
181
|
+
return buildCatalog();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function getComponent(typeName) {
|
|
185
|
+
const cat = await buildCatalog();
|
|
186
|
+
if (cat.entries.has(typeName)) return cat.entries.get(typeName);
|
|
187
|
+
const tag = registry.get(typeName);
|
|
188
|
+
if (!tag) return null;
|
|
189
|
+
for (const entry of cat.entries.values()) {
|
|
190
|
+
if (entry.tag === tag) return entry;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function getComponentsByCategory(category) {
|
|
196
|
+
const cat = await buildCatalog();
|
|
197
|
+
return [...cat.entries.values()].filter(e => e.category === category);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function getTraits() {
|
|
201
|
+
return traitRegistry;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function getTraitsByCategory(category) {
|
|
205
|
+
return traitRegistry.filter(t => t.category === category);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function getFullCatalog() {
|
|
209
|
+
const cat = await buildCatalog();
|
|
210
|
+
return {
|
|
211
|
+
...cat,
|
|
212
|
+
totalTraits: traitRegistry.length,
|
|
213
|
+
traits: traitRegistry,
|
|
214
|
+
};
|
|
215
|
+
}
|
package/clarity.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clarity Assessment — Evaluates whether a user intent is clear enough
|
|
3
|
+
* to generate quality UI, or needs clarifying questions first.
|
|
4
|
+
*
|
|
5
|
+
* Scores intents across 5 dimensions:
|
|
6
|
+
* domain — Is the domain clear? (forms, data, layout, etc.)
|
|
7
|
+
* scope — Is the scope defined? (how many items, what sections?)
|
|
8
|
+
* content — Is the content specified? (labels, values, data?)
|
|
9
|
+
* layout — Is the layout preference stated? (grid, stack, sidebar?)
|
|
10
|
+
* action — Are actions/interactions mentioned? (buttons, forms, links?)
|
|
11
|
+
*
|
|
12
|
+
* Returns a clarity score (0-1) and targeted questions for what's missing.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { classifyIntent } from './domain-router.js';
|
|
16
|
+
import { searchPatterns } from './pattern-library.js';
|
|
17
|
+
|
|
18
|
+
// ── Dimension detectors ──────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/** Scope indicators: quantity, enumeration, specificity */
|
|
21
|
+
const SCOPE_SIGNALS = [
|
|
22
|
+
/\b\d+\b/, // contains a number
|
|
23
|
+
/\b(three|two|four|five|six)\b/i, // spelled-out numbers
|
|
24
|
+
/\b(with|containing|including|showing|displaying)\b/i, // enumerates content
|
|
25
|
+
/\b(and|plus|also)\b/i, // multiple items
|
|
26
|
+
/\b(columns?|rows?|items?|cards?|sections?|fields?|buttons?|tabs?|steps?)\b/i, // structural counts
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** Content specificity: labels, values, named data */
|
|
30
|
+
const CONTENT_SIGNALS = [
|
|
31
|
+
/\b(name|email|password|title|description|price|date|status|role|avatar)\b/i,
|
|
32
|
+
/\b(revenue|users|growth|sales|orders|metrics|analytics)\b/i,
|
|
33
|
+
/\b(todo|done|in progress|pending|active|completed)\b/i,
|
|
34
|
+
/\b(bleed|margin|trim|crop|preview|artwork|brand|design system)\b/i,
|
|
35
|
+
/\b(approved|approval|production|settings|configure|preference)\b/i,
|
|
36
|
+
/["'][\w\s]+["']/, // quoted strings (specific labels)
|
|
37
|
+
/\$[\d,.]+/, // dollar amounts
|
|
38
|
+
/\d+%/, // percentages
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/** Layout preferences */
|
|
42
|
+
const LAYOUT_SIGNALS = [
|
|
43
|
+
/\b(grid|row|column|sidebar|split|horizontal|vertical|stack|centered)\b/i,
|
|
44
|
+
/\b(full.?width|responsive|mobile|compact|wide|narrow)\b/i,
|
|
45
|
+
/\b(\d+.?col(umn)?s?)\b/i, // "3 columns", "2-col"
|
|
46
|
+
/\b(left|right|top|bottom|center)\b/i,
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/** Action/interaction indicators */
|
|
50
|
+
const ACTION_SIGNALS = [
|
|
51
|
+
/\b(button|submit|cancel|save|delete|edit|close|open|toggle|click)\b/i,
|
|
52
|
+
/\b(form|input|select|search|filter|sort|upload|download)\b/i,
|
|
53
|
+
/\b(login|signup|register|checkout|confirm|approve|reject)\b/i,
|
|
54
|
+
/\b(navigate|link|redirect|route)\b/i,
|
|
55
|
+
/\b(drag|drop|resize|expand|collapse)\b/i,
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Assess clarity of a user intent for UI generation.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} intent — Raw user input
|
|
62
|
+
* @param {{ domain: string, confidence: number, matchedSignals: string[] }} [classification] — Pre-computed classification
|
|
63
|
+
* @returns {{
|
|
64
|
+
* clear: boolean,
|
|
65
|
+
* score: number,
|
|
66
|
+
* dimensions: { domain: number, scope: number, content: number, layout: number, action: number },
|
|
67
|
+
* questions: { text: string, dimension: string, priority: number }[],
|
|
68
|
+
* summary: string,
|
|
69
|
+
* }}
|
|
70
|
+
*/
|
|
71
|
+
export function assessClarity(intent, classification) {
|
|
72
|
+
const text = (intent ?? '').trim();
|
|
73
|
+
if (!text) {
|
|
74
|
+
return {
|
|
75
|
+
clear: false,
|
|
76
|
+
score: 0,
|
|
77
|
+
dimensions: { domain: 0, scope: 0, content: 0, layout: 0, action: 0 },
|
|
78
|
+
questions: [{ text: 'What would you like me to build?', dimension: 'domain', priority: 1 }],
|
|
79
|
+
summary: 'No intent provided',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const cls = classification || classifyIntent(text);
|
|
84
|
+
const patterns = searchPatterns(text);
|
|
85
|
+
const lower = text.toLowerCase();
|
|
86
|
+
const wordCount = text.split(/\s+/).length;
|
|
87
|
+
|
|
88
|
+
// ── Score each dimension ──
|
|
89
|
+
|
|
90
|
+
// Domain: how confident is the domain classification?
|
|
91
|
+
const domainScore = cls.confidence >= 0.3 ? 1 : cls.confidence >= 0.15 ? 0.6 : cls.matchedSignals.length > 0 ? 0.3 : 0;
|
|
92
|
+
|
|
93
|
+
// Scope: is the scope specific?
|
|
94
|
+
const scopeHits = SCOPE_SIGNALS.filter(r => r.test(text)).length;
|
|
95
|
+
const scopeScore = Math.min(1, scopeHits / 2);
|
|
96
|
+
|
|
97
|
+
// Content: are concrete labels/values mentioned?
|
|
98
|
+
const contentHits = CONTENT_SIGNALS.filter(r => r.test(text)).length;
|
|
99
|
+
const contentScore = Math.min(1, contentHits / 2);
|
|
100
|
+
|
|
101
|
+
// Layout: is a layout preference stated?
|
|
102
|
+
const layoutHits = LAYOUT_SIGNALS.filter(r => r.test(text)).length;
|
|
103
|
+
const layoutScore = Math.min(1, layoutHits);
|
|
104
|
+
|
|
105
|
+
// Action: are interactions mentioned?
|
|
106
|
+
const actionHits = ACTION_SIGNALS.filter(r => r.test(text)).length;
|
|
107
|
+
const actionScore = Math.min(1, actionHits / 2);
|
|
108
|
+
|
|
109
|
+
// ── Overall score (weighted) ──
|
|
110
|
+
const score = (
|
|
111
|
+
domainScore * 0.25 +
|
|
112
|
+
scopeScore * 0.25 +
|
|
113
|
+
contentScore * 0.20 +
|
|
114
|
+
layoutScore * 0.15 +
|
|
115
|
+
actionScore * 0.15
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// ── Bonus: pattern match adds confidence ──
|
|
119
|
+
const patternBonus = patterns.length > 0 ? 0.15 : 0;
|
|
120
|
+
// Bonus: long intents are usually more specific
|
|
121
|
+
const lengthBonus = wordCount > 10 ? 0.1 : wordCount > 6 ? 0.05 : 0;
|
|
122
|
+
|
|
123
|
+
const finalScore = Math.min(1, score + patternBonus + lengthBonus);
|
|
124
|
+
|
|
125
|
+
// ── Generate targeted questions for weak dimensions ──
|
|
126
|
+
const questions = [];
|
|
127
|
+
|
|
128
|
+
if (domainScore < 0.3) {
|
|
129
|
+
questions.push({
|
|
130
|
+
text: 'What type of UI is this? (e.g., a form, dashboard, profile card, settings page)',
|
|
131
|
+
dimension: 'domain',
|
|
132
|
+
priority: 1,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (scopeScore < 0.5) {
|
|
137
|
+
const domainQuestions = {
|
|
138
|
+
forms: 'What fields should the form include?',
|
|
139
|
+
data: 'What data should be displayed? How many items or metrics?',
|
|
140
|
+
layout: 'How many sections or cards should it have?',
|
|
141
|
+
agent: 'What actions should the assistant support?',
|
|
142
|
+
navigation: 'What pages or sections should be navigable?',
|
|
143
|
+
};
|
|
144
|
+
questions.push({
|
|
145
|
+
text: domainQuestions[cls.domain] || 'Can you be more specific about what it should contain?',
|
|
146
|
+
dimension: 'scope',
|
|
147
|
+
priority: 2,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (contentScore < 0.3 && scopeScore >= 0.3) {
|
|
152
|
+
const contentQuestions = {
|
|
153
|
+
forms: 'What labels should the fields have? (e.g., "Email", "Password", "Full Name")',
|
|
154
|
+
data: 'What metrics or values should be shown? (e.g., revenue, users, growth rate)',
|
|
155
|
+
layout: 'What content goes in each section?',
|
|
156
|
+
agent: 'What kind of messages or responses should be shown?',
|
|
157
|
+
navigation: 'What should the menu items or links be labeled?',
|
|
158
|
+
};
|
|
159
|
+
questions.push({
|
|
160
|
+
text: contentQuestions[cls.domain] || 'What specific content should be displayed?',
|
|
161
|
+
dimension: 'content',
|
|
162
|
+
priority: 3,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (layoutScore < 0.3 && scopeScore >= 0.5) {
|
|
167
|
+
questions.push({
|
|
168
|
+
text: 'Any layout preference? (e.g., grid of cards, single column, sidebar + content)',
|
|
169
|
+
dimension: 'layout',
|
|
170
|
+
priority: 4,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (actionScore < 0.3 && domainScore >= 0.3) {
|
|
175
|
+
questions.push({
|
|
176
|
+
text: 'What actions should users be able to take? (e.g., submit, edit, delete, filter)',
|
|
177
|
+
dimension: 'action',
|
|
178
|
+
priority: 5,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Sort by priority, limit to 3
|
|
183
|
+
questions.sort((a, b) => a.priority - b.priority);
|
|
184
|
+
const topQuestions = questions.slice(0, 3);
|
|
185
|
+
|
|
186
|
+
// ── Determine if clear enough to proceed ──
|
|
187
|
+
// Clear only if dimensional score meets threshold
|
|
188
|
+
const clear = finalScore >= 0.4;
|
|
189
|
+
|
|
190
|
+
const summary = clear
|
|
191
|
+
? `Intent is ${finalScore >= 0.7 ? 'clear' : 'adequate'} (${Math.round(finalScore * 100)}%)`
|
|
192
|
+
: `Intent needs clarification (${Math.round(finalScore * 100)}% — below 40% threshold)`;
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
clear,
|
|
196
|
+
score: Math.round(finalScore * 100) / 100,
|
|
197
|
+
dimensions: {
|
|
198
|
+
domain: Math.round(domainScore * 100) / 100,
|
|
199
|
+
scope: Math.round(scopeScore * 100) / 100,
|
|
200
|
+
content: Math.round(contentScore * 100) / 100,
|
|
201
|
+
layout: Math.round(layoutScore * 100) / 100,
|
|
202
|
+
action: Math.round(actionScore * 100) / 100,
|
|
203
|
+
},
|
|
204
|
+
questions: topQuestions,
|
|
205
|
+
summary,
|
|
206
|
+
};
|
|
207
|
+
}
|