@adia-ai/a2ui-retrieval 0.6.6 → 0.6.8
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 +12 -0
- package/index.d.ts +4 -0
- package/package.json +4 -2
- 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 -175
- 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
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Prompt Analyzer — baseline ingestion stage for the gen-ui pipeline.
|
|
3
|
-
*
|
|
4
|
-
* Every user intent enters through here, regardless of engine (monolithic,
|
|
5
|
-
* zettel) or mode (instant, pro, thinking). The analyzer extracts structured
|
|
6
|
-
* signals from the raw prompt — what concepts the user is gesturing at, which
|
|
7
|
-
* AdiaUI components are implied, what implicit requirements a good designer
|
|
8
|
-
* would add — and produces a "steelmanned" enriched brief that downstream
|
|
9
|
-
* stages reason against.
|
|
10
|
-
*
|
|
11
|
-
* Output schema:
|
|
12
|
-
* {
|
|
13
|
-
* raw: string // user's original intent, verbatim
|
|
14
|
-
* concepts: string[] // 2-5 short concept tags ("commerce", "auth", "data-display")
|
|
15
|
-
* entities: string[] // explicit nouns from the prompt ("image", "title", "price")
|
|
16
|
-
* impliedComponents: string[] // 4-12 AdiaUI component names ("Card", "Image", "Button")
|
|
17
|
-
* styleHints: string[] // visual/style hints ("3D", "minimal", "dense"), [] if none
|
|
18
|
-
* steelman: string // 1-2 sentence enriched brief
|
|
19
|
-
* analyzed: boolean // true if LLM call succeeded; false if fell back to passthrough
|
|
20
|
-
* }
|
|
21
|
-
*
|
|
22
|
-
* Failure mode: if the LLM call throws or returns unparseable JSON, we
|
|
23
|
-
* gracefully degrade to a passthrough analysis (steelman = raw intent, no
|
|
24
|
-
* extracted signals). Generation continues; quality may suffer but nothing
|
|
25
|
-
* breaks.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
// Node's `process` global is not in scope under `"types": []` — declare
|
|
29
|
-
// minimally for the typeof guard below (erased at runtime by erasableSyntaxOnly).
|
|
30
|
-
declare const process: { versions?: { node?: string } } | undefined;
|
|
31
|
-
|
|
32
|
-
// Component vocabulary derived from the catalog. Two artifacts:
|
|
33
|
-
// - displayList: human-readable list shown to the LLM, with aliases inlined
|
|
34
|
-
// - allowedSet: canonical AND alias names accepted in impliedComponents
|
|
35
|
-
let _vocab: { displayList: string[]; allowedSet: Set<string> } | null = null;
|
|
36
|
-
|
|
37
|
-
async function getVocab(): Promise<{ displayList: string[]; allowedSet: Set<string> }> {
|
|
38
|
-
if (_vocab) return _vocab;
|
|
39
|
-
let catalogJson: Record<string, unknown> = {};
|
|
40
|
-
try {
|
|
41
|
-
const IS_NODE = typeof process !== 'undefined' && process.versions?.node;
|
|
42
|
-
if (IS_NODE) {
|
|
43
|
-
const fs = await import(/* @vite-ignore */ 'node:fs/promises');
|
|
44
|
-
const path = await import(/* @vite-ignore */ 'node:path');
|
|
45
|
-
const url = await import(/* @vite-ignore */ 'node:url');
|
|
46
|
-
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
47
|
-
const raw = await fs.readFile(path.join(__dirname, '../../corpus/catalog-a2ui_0_9.json'), 'utf8');
|
|
48
|
-
catalogJson = JSON.parse(raw) as Record<string, unknown>;
|
|
49
|
-
} else {
|
|
50
|
-
const resp = await fetch(new URL('../../corpus/catalog-a2ui_0_9.json', import.meta.url));
|
|
51
|
-
if (resp.ok) catalogJson = await resp.json() as Record<string, unknown>;
|
|
52
|
-
}
|
|
53
|
-
} catch { /* empty vocab — analyzer will still work, just unconstrained */ }
|
|
54
|
-
|
|
55
|
-
// Adapt v0.9 catalog shape
|
|
56
|
-
const comps = (catalogJson?.['components'] ?? {}) as Record<string, unknown>;
|
|
57
|
-
const displayList: string[] = [];
|
|
58
|
-
const allowedSet = new Set<string>();
|
|
59
|
-
for (const [name, def] of Object.entries(comps)) {
|
|
60
|
-
allowedSet.add(name);
|
|
61
|
-
const ext = (def as Record<string, unknown>)?.['x-adiaui'] ?? {};
|
|
62
|
-
const syns = (ext && typeof ext === 'object') ? (ext as Record<string, unknown>)['synonyms'] : null;
|
|
63
|
-
const tags = (syns && typeof syns === 'object') ? (syns as Record<string, unknown>)['tags'] : null;
|
|
64
|
-
const aliases: string[] = Array.isArray(tags) ? (tags as unknown[]).filter((t): t is string => typeof t === 'string') : [];
|
|
65
|
-
for (const a of aliases) allowedSet.add(a);
|
|
66
|
-
displayList.push(aliases.length ? `${name} (also: ${aliases.join(', ')})` : name);
|
|
67
|
-
}
|
|
68
|
-
_vocab = { displayList, allowedSet };
|
|
69
|
-
return _vocab;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export type PromptAnalysis = {
|
|
73
|
-
raw: string;
|
|
74
|
-
concepts: string[];
|
|
75
|
-
entities: string[];
|
|
76
|
-
impliedComponents: string[];
|
|
77
|
-
styleHints: string[];
|
|
78
|
-
steelman: string;
|
|
79
|
-
analyzed: boolean;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
type LLMAdapter = {
|
|
83
|
-
complete(opts: { messages: Array<{ role: string; content: string }>; systemPrompt: string }): Promise<{ content?: string; stopReason?: string }>;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
type DomainHint = {
|
|
87
|
-
domain?: string;
|
|
88
|
-
confidence?: number;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Analyze a user prompt — extract concepts, entities, implied components,
|
|
93
|
-
* style hints, and produce a steelmanned brief.
|
|
94
|
-
*/
|
|
95
|
-
export async function analyzePrompt({ intent, llmAdapter, domain }: {
|
|
96
|
-
intent: string;
|
|
97
|
-
llmAdapter?: LLMAdapter;
|
|
98
|
-
domain?: DomainHint;
|
|
99
|
-
}): Promise<PromptAnalysis> {
|
|
100
|
-
const passthrough: PromptAnalysis = {
|
|
101
|
-
raw: intent,
|
|
102
|
-
concepts: [],
|
|
103
|
-
entities: [],
|
|
104
|
-
impliedComponents: [],
|
|
105
|
-
styleHints: [],
|
|
106
|
-
steelman: intent,
|
|
107
|
-
analyzed: false,
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
if (!intent || !llmAdapter) return passthrough;
|
|
111
|
-
|
|
112
|
-
const { displayList, allowedSet } = await getVocab();
|
|
113
|
-
const componentList = displayList.length
|
|
114
|
-
? displayList.join(', ')
|
|
115
|
-
: 'Card, Section, Header, Footer, Column, Row, Grid, Button, Input, Text, Image, Badge, Icon, Avatar, Alert, Modal, Tabs, Accordion';
|
|
116
|
-
|
|
117
|
-
const domainHint = domain?.['domain'] && (domain?.['confidence'] ?? 0) > 0
|
|
118
|
-
? `\nDomain classifier suggests: ${domain['domain']} (confidence ${domain['confidence']})`
|
|
119
|
-
: '';
|
|
120
|
-
|
|
121
|
-
const systemPrompt = `You are a UI brief enricher for the AdiaUI design system. Given a terse or under-specified user prompt, you extract structured signals and produce an enriched brief that a downstream UI generator can use to produce a polished, complete UI.
|
|
122
|
-
|
|
123
|
-
Your output is JSON with this exact shape — no markdown, no explanation:
|
|
124
|
-
{
|
|
125
|
-
"concepts": [string], // 2-5 short concept tags (e.g. "commerce", "authentication", "data-display", "navigation")
|
|
126
|
-
"entities": [string], // explicit nouns from the prompt (lowercase, singular)
|
|
127
|
-
"implied_components": [string],// 4-12 AdiaUI components a good designer would include
|
|
128
|
-
"style_hints": [string], // visual/style adjectives ("minimal", "dense", "3D", "playful"), [] if none stated
|
|
129
|
-
"steelman": string // 1-2 sentence enriched brief
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
AdiaUI components you may name in implied_components (use these names exactly): ${componentList}
|
|
133
|
-
|
|
134
|
-
Rules:
|
|
135
|
-
- Extract entities verbatim from the prompt — do not invent.
|
|
136
|
-
- implied_components should include both the obvious (Card, Button) AND the ones a thoughtful designer would add (Badge for sale tag on a product, Avatar for a user row, EmptyState for empty data).
|
|
137
|
-
- The steelman expands the prompt into what the user MEANT, not what they SAID. Add the implicit requirements: typical sub-elements, sensible defaults, a header, a footer, common variants. Do NOT add scope the user wouldn't want (e.g. don't turn "login form" into "login form with social auth" unless that's a near-universal expectation).
|
|
138
|
-
- Keep steelman under 250 characters.
|
|
139
|
-
- If the prompt is already well-specified (>15 words, multiple entities), the steelman is mostly a faithful restatement.
|
|
140
|
-
|
|
141
|
-
Examples:
|
|
142
|
-
Input: "product card"
|
|
143
|
-
Output: {"concepts":["commerce","product-display"],"entities":["product"],"implied_components":["Card","Image","Text","Badge","Button"],"style_hints":[],"steelman":"E-commerce product card with image, title, price, optional sale badge, and primary 'Add to cart' button. Card layout, single product."}
|
|
144
|
-
|
|
145
|
-
Input: "settings page with toggles for notifications, dark mode, and privacy"
|
|
146
|
-
Output: {"concepts":["settings","preferences"],"entities":["notifications","dark mode","privacy","toggles"],"implied_components":["Card","Header","Section","Toggle","Text","Divider"],"style_hints":[],"steelman":"Settings page with three toggle rows for notifications, dark mode, and privacy. Each row has a label, description, and toggle. Grouped under a header card."}
|
|
147
|
-
|
|
148
|
-
Input: "make it 3D"
|
|
149
|
-
Output: {"concepts":["style-modifier"],"entities":[],"implied_components":[],"style_hints":["3D","depth","shadow"],"steelman":"Modify the existing canvas to feel three-dimensional — add elevation, layered shadows, and depth-suggesting transforms. Apply to existing components without changing structure."}`;
|
|
150
|
-
|
|
151
|
-
const userMsg = `Prompt: "${intent}"${domainHint}`;
|
|
152
|
-
|
|
153
|
-
let response: { content?: string; stopReason?: string } | undefined;
|
|
154
|
-
try {
|
|
155
|
-
response = await llmAdapter.complete({
|
|
156
|
-
messages: [{ role: 'user', content: userMsg }],
|
|
157
|
-
systemPrompt,
|
|
158
|
-
});
|
|
159
|
-
} catch {
|
|
160
|
-
return passthrough;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const text = response?.content;
|
|
164
|
-
if (!text) return passthrough;
|
|
165
|
-
|
|
166
|
-
// Truncation: if the analyzer LLM hit max_tokens we'd rather use the raw
|
|
167
|
-
// prompt than a half-parsed analysis that misleads downstream stages.
|
|
168
|
-
if (response.stopReason && response.stopReason !== 'end' && response.stopReason !== 'end_turn' && response.stopReason !== 'stop') {
|
|
169
|
-
return passthrough;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Try direct parse, then code-fence extraction, then outermost JSON.
|
|
173
|
-
let parsed: unknown = null;
|
|
174
|
-
try { parsed = JSON.parse(text.trim()); } catch { /* fall through */ }
|
|
175
|
-
if (!parsed) {
|
|
176
|
-
const fence = text.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
|
|
177
|
-
if (fence) try { parsed = JSON.parse((fence[1] ?? '').trim()); } catch { /* fall through */ }
|
|
178
|
-
}
|
|
179
|
-
if (!parsed) {
|
|
180
|
-
const start = text.indexOf('{');
|
|
181
|
-
const end = text.lastIndexOf('}');
|
|
182
|
-
if (start >= 0 && end > start) {
|
|
183
|
-
try { parsed = JSON.parse(text.slice(start, end + 1)); } catch { /* fall through */ }
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
if (!parsed || typeof parsed !== 'object') return passthrough;
|
|
187
|
-
|
|
188
|
-
const p = parsed as Record<string, unknown>;
|
|
189
|
-
|
|
190
|
-
// Constrain impliedComponents to the allowed vocab
|
|
191
|
-
const impliedComponents = Array.isArray(p['implied_components'])
|
|
192
|
-
? (p['implied_components'] as unknown[]).filter((c): c is string => typeof c === 'string' && (allowedSet.size === 0 || allowedSet.has(c)))
|
|
193
|
-
: [];
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
raw: intent,
|
|
197
|
-
concepts: asStringArray(p['concepts']).slice(0, 8),
|
|
198
|
-
entities: asStringArray(p['entities']).slice(0, 16),
|
|
199
|
-
impliedComponents: impliedComponents.slice(0, 16),
|
|
200
|
-
styleHints: asStringArray(p['style_hints']).slice(0, 8),
|
|
201
|
-
steelman: (typeof p['steelman'] === 'string' && p['steelman'].trim()) || intent,
|
|
202
|
-
analyzed: true,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function asStringArray(v: unknown): string[] {
|
|
207
|
-
if (!Array.isArray(v)) return [];
|
|
208
|
-
return (v as unknown[]).filter((x): x is string => typeof x === 'string' && x.trim().length > 0).map(x => (x as string).trim());
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Format an analysis object for inclusion in a downstream system prompt.
|
|
213
|
-
* Keeps the shape compact so it doesn't blow the context budget.
|
|
214
|
-
*/
|
|
215
|
-
export function formatAnalysisForPrompt(analysis: PromptAnalysis | null | undefined): string {
|
|
216
|
-
if (!analysis || !analysis.analyzed) return '';
|
|
217
|
-
const parts: string[] = [];
|
|
218
|
-
if (analysis.steelman && analysis.steelman !== analysis.raw) {
|
|
219
|
-
parts.push(`ENRICHED BRIEF: ${analysis.steelman}`);
|
|
220
|
-
}
|
|
221
|
-
if (analysis.impliedComponents?.length) {
|
|
222
|
-
parts.push(`EXPECTED COMPONENTS: ${analysis.impliedComponents.join(', ')}`);
|
|
223
|
-
}
|
|
224
|
-
if (analysis.concepts?.length) {
|
|
225
|
-
parts.push(`CONCEPTS: ${analysis.concepts.join(', ')}`);
|
|
226
|
-
}
|
|
227
|
-
if (analysis.styleHints?.length) {
|
|
228
|
-
parts.push(`STYLE HINTS: ${analysis.styleHints.join(', ')}`);
|
|
229
|
-
}
|
|
230
|
-
return parts.join('\n');
|
|
231
|
-
}
|