@adia-ai/a2ui-retrieval 0.6.4 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/domain-router.js +362 -117
- package/embedding/chunk-embedding-retriever.js +47 -79
- package/embedding/embedding-provider.js +35 -71
- package/embedding/index.js +2 -10
- package/feedback/dialog-recorder.js +61 -145
- package/feedback/feedback-analyzer.js +46 -102
- package/feedback/feedback-store.js +91 -107
- package/feedback/feedback.js +36 -117
- package/feedback/gap-registry.js +40 -82
- package/feedback/index.js +14 -12
- package/index.d.ts +4 -0
- package/index.js +53 -16
- package/intent/clarity.js +61 -129
- package/intent/decomposer.js +51 -143
- package/intent/index.js +18 -14
- package/intent/intent-alignment.js +79 -150
- package/intent/intent-categorizer.js +34 -62
- package/intent/intent-gate.js +43 -102
- package/intent/prompt-analyzer.js +68 -126
- package/package.json +4 -2
- package/wiring-catalog.js +95 -146
- package/embedding/chunk-embedding-retriever.ts +0 -156
- package/embedding/embedding-provider.ts +0 -111
- package/embedding/index.ts +0 -10
- package/feedback/dialog-recorder.ts +0 -172
- package/feedback/feedback-analyzer.ts +0 -250
- package/feedback/feedback-store.ts +0 -229
- package/feedback/feedback.ts +0 -201
- package/feedback/gap-registry.ts +0 -137
- package/feedback/index.ts +0 -14
- package/intent/clarity.ts +0 -224
- package/intent/decomposer.ts +0 -229
- package/intent/index.ts +0 -20
- package/intent/intent-alignment.ts +0 -267
- package/intent/intent-categorizer.ts +0 -104
- package/intent/intent-gate.ts +0 -151
- package/intent/prompt-analyzer.ts +0 -231
package/feedback/feedback.ts
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FeedbackCollector — Structured feedback for the evolution engine.
|
|
3
|
-
*
|
|
4
|
-
* Captures per-generation feedback across multiple dimensions:
|
|
5
|
-
* - Overall rating (1-5)
|
|
6
|
-
* - Intent alignment, visual quality, component choice (1-5 each)
|
|
7
|
-
* - Whether the user edited the output
|
|
8
|
-
* - Pattern promotion signals ("this should become a pattern")
|
|
9
|
-
*
|
|
10
|
-
* Exports as a structured JSON log for the training cycle.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
export type QualityDimensions = {
|
|
14
|
-
structural: number;
|
|
15
|
-
completeness: number;
|
|
16
|
-
idiomatic: number;
|
|
17
|
-
minimal: number;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export type ValidationCheck = { name: string; passed: boolean };
|
|
21
|
-
|
|
22
|
-
export type GenerationData = {
|
|
23
|
-
componentCount: number;
|
|
24
|
-
componentTypes: string[];
|
|
25
|
-
score: number;
|
|
26
|
-
validationChecks: ValidationCheck[];
|
|
27
|
-
qualityDimensions: QualityDimensions;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export type FeedbackData = {
|
|
31
|
-
rating?: number;
|
|
32
|
-
intentAlignment?: number;
|
|
33
|
-
visualQuality?: number;
|
|
34
|
-
componentChoice?: number;
|
|
35
|
-
userEdited?: boolean;
|
|
36
|
-
editSummary?: string;
|
|
37
|
-
notes?: string;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export type PatternData = {
|
|
41
|
-
patternUsed?: string;
|
|
42
|
-
shouldBePattern?: boolean;
|
|
43
|
-
suggestedName?: string;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export type FeedbackEntry = {
|
|
47
|
-
executionId: string;
|
|
48
|
-
intent: string;
|
|
49
|
-
domain: string;
|
|
50
|
-
mode: string;
|
|
51
|
-
timestamp: number;
|
|
52
|
-
generation: GenerationData;
|
|
53
|
-
feedback: FeedbackData;
|
|
54
|
-
patterns: PatternData;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
type ComponentLike = {
|
|
58
|
-
component?: string;
|
|
59
|
-
[key: string]: unknown;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
type ValidationResult = {
|
|
63
|
-
score?: number;
|
|
64
|
-
checks?: Array<{ name: string; passed: boolean }>;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
export class FeedbackCollector {
|
|
68
|
-
readonly #entries = new Map<string, FeedbackEntry>();
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Initialize a feedback entry from generation results.
|
|
72
|
-
* Called automatically after each generation completes.
|
|
73
|
-
*/
|
|
74
|
-
initFromGeneration(executionId: string, { intent, domain, mode, messages, validation }: {
|
|
75
|
-
intent?: string;
|
|
76
|
-
domain?: string;
|
|
77
|
-
mode?: string;
|
|
78
|
-
messages?: Array<{ components?: ComponentLike[] }>;
|
|
79
|
-
validation?: ValidationResult;
|
|
80
|
-
}): void {
|
|
81
|
-
const components: ComponentLike[] = messages?.[0]?.components ?? [];
|
|
82
|
-
const checks = validation?.checks ?? [];
|
|
83
|
-
|
|
84
|
-
// Compute quality dimensions
|
|
85
|
-
const failedChecks = checks.filter(c => !c.passed);
|
|
86
|
-
const structural = failedChecks.some(c =>
|
|
87
|
-
['hasRootComponent', 'noOrphanedChildren', 'flatAdjacency'].includes(c.name)
|
|
88
|
-
) ? 0.5 : 1;
|
|
89
|
-
const completeness = Math.max(0, 1 - (
|
|
90
|
-
failedChecks.filter(c => ['textContentSet', 'allTypesRegistered'].includes(c.name)).length * 0.1
|
|
91
|
-
));
|
|
92
|
-
const idiomatic = failedChecks.some(c =>
|
|
93
|
-
['noBareDivs', 'noBareInputs', 'cardStructure'].includes(c.name)
|
|
94
|
-
) ? 0.5 : 1;
|
|
95
|
-
const minimal = failedChecks.some(c =>
|
|
96
|
-
['noHardcodedColors', 'noInlineLayout'].includes(c.name)
|
|
97
|
-
) ? 0.5 : 1;
|
|
98
|
-
|
|
99
|
-
this.#entries.set(executionId, {
|
|
100
|
-
executionId,
|
|
101
|
-
intent: intent ?? '',
|
|
102
|
-
domain: domain ?? '',
|
|
103
|
-
mode: mode ?? 'instant',
|
|
104
|
-
timestamp: Date.now(),
|
|
105
|
-
generation: {
|
|
106
|
-
componentCount: components.length,
|
|
107
|
-
componentTypes: [...new Set(components.map(c => c.component).filter((x): x is string => Boolean(x)))],
|
|
108
|
-
score: validation?.score ?? 0,
|
|
109
|
-
validationChecks: checks.map(c => ({ name: c.name, passed: c.passed })),
|
|
110
|
-
qualityDimensions: { structural, completeness, idiomatic, minimal },
|
|
111
|
-
},
|
|
112
|
-
feedback: {},
|
|
113
|
-
patterns: {},
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Collect user feedback for an execution.
|
|
119
|
-
*/
|
|
120
|
-
collectFeedback(executionId: string, feedback: FeedbackData): void {
|
|
121
|
-
const entry = this.#entries.get(executionId);
|
|
122
|
-
if (!entry) {
|
|
123
|
-
// Create a minimal entry if init wasn't called
|
|
124
|
-
this.#entries.set(executionId, {
|
|
125
|
-
executionId,
|
|
126
|
-
intent: '', domain: '', mode: '', timestamp: Date.now(),
|
|
127
|
-
generation: { componentCount: 0, componentTypes: [], score: 0, validationChecks: [], qualityDimensions: { structural: 0, completeness: 0, idiomatic: 0, minimal: 0 } },
|
|
128
|
-
feedback: {},
|
|
129
|
-
patterns: {},
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
const e = this.#entries.get(executionId)!;
|
|
133
|
-
e.feedback = { ...e.feedback, ...feedback };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Collect pattern-related feedback.
|
|
138
|
-
*/
|
|
139
|
-
collectPatternFeedback(executionId: string, patternFeedback: PatternData): void {
|
|
140
|
-
const entry = this.#entries.get(executionId);
|
|
141
|
-
if (!entry) return;
|
|
142
|
-
entry.patterns = { ...entry.patterns, ...patternFeedback };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Get a single feedback entry.
|
|
147
|
-
*/
|
|
148
|
-
get(executionId: string): FeedbackEntry | null {
|
|
149
|
-
return this.#entries.get(executionId) ?? null;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Get all feedback entries.
|
|
154
|
-
*/
|
|
155
|
-
getAll(): FeedbackEntry[] {
|
|
156
|
-
return [...this.#entries.values()];
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Number of entries. */
|
|
160
|
-
get size(): number {
|
|
161
|
-
return this.#entries.size;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Export all feedback as structured JSON.
|
|
166
|
-
* In browser: triggers a file download.
|
|
167
|
-
* In Node: returns the JSON string.
|
|
168
|
-
*/
|
|
169
|
-
exportFeedback(): string {
|
|
170
|
-
const data = {
|
|
171
|
-
exportedAt: new Date().toISOString(),
|
|
172
|
-
entryCount: this.#entries.size,
|
|
173
|
-
entries: this.getAll(),
|
|
174
|
-
};
|
|
175
|
-
const json = JSON.stringify(data, null, 2);
|
|
176
|
-
|
|
177
|
-
// Browser download
|
|
178
|
-
if (typeof document !== 'undefined') {
|
|
179
|
-
const date = new Date().toISOString().slice(0, 10);
|
|
180
|
-
const blob = new Blob([json], { type: 'application/json' });
|
|
181
|
-
const url = URL.createObjectURL(blob);
|
|
182
|
-
const a = document.createElement('a');
|
|
183
|
-
a.href = url;
|
|
184
|
-
a.download = `gen-ui-feedback-${date}.json`;
|
|
185
|
-
a.style.display = 'none';
|
|
186
|
-
// Prevent SPA router from intercepting the blob URL click
|
|
187
|
-
a.addEventListener('click', (e) => e.stopPropagation());
|
|
188
|
-
document.body.appendChild(a);
|
|
189
|
-
a.click();
|
|
190
|
-
document.body.removeChild(a);
|
|
191
|
-
URL.revokeObjectURL(url);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return json;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** Clear all entries. */
|
|
198
|
-
clear(): void {
|
|
199
|
-
this.#entries.clear();
|
|
200
|
-
}
|
|
201
|
-
}
|
package/feedback/gap-registry.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Gap Registry
|
|
3
|
-
*
|
|
4
|
-
* Manages a persistent JSON file tracking identified pattern/training gaps.
|
|
5
|
-
* Used by the feedback pipeline to record and track resolution of weak areas.
|
|
6
|
-
*
|
|
7
|
-
* Registry file: packages/a2ui/corpus/gaps/registry.json
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* import { loadGaps, addGap, updateGapStatus } from './gap-registry.js';
|
|
11
|
-
* const gaps = await loadGaps();
|
|
12
|
-
* await addGap({ intentCategory: 'form/checkout', ... });
|
|
13
|
-
* await updateGapStatus('form/checkout', 'resolved', 'Added checkout pattern');
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
// `process` is not in scope under "types": []
|
|
17
|
-
declare const process: { versions?: { node?: string } } | undefined;
|
|
18
|
-
|
|
19
|
-
// Lazy top-level import pattern for Node-only modules
|
|
20
|
-
let fs: typeof import('node:fs/promises') | null = null;
|
|
21
|
-
let path: typeof import('node:path') | null = null;
|
|
22
|
-
const IS_NODE = typeof process !== 'undefined' && process.versions?.node;
|
|
23
|
-
if (IS_NODE) {
|
|
24
|
-
try {
|
|
25
|
-
fs = await import(/* @vite-ignore */ 'node:fs/promises');
|
|
26
|
-
path = await import(/* @vite-ignore */ 'node:path');
|
|
27
|
-
} catch {
|
|
28
|
-
// Node builtins unavailable
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// packages/a2ui/retrieval/feedback → up 3 → packages/a2ui → corpus/gaps/registry.json
|
|
33
|
-
const REGISTRY_PATH: string | null = path
|
|
34
|
-
? path.join(path.dirname(new URL(import.meta.url).pathname), '..', '..', '..', 'a2ui/corpus', 'gaps', 'registry.json')
|
|
35
|
-
: null;
|
|
36
|
-
|
|
37
|
-
export type GapStatus = 'open' | 'in-progress' | 'resolved';
|
|
38
|
-
|
|
39
|
-
export type Gap = {
|
|
40
|
-
intentCategory: string;
|
|
41
|
-
detectedAt: string;
|
|
42
|
-
lastSeen: string;
|
|
43
|
-
sampleCount: number;
|
|
44
|
-
avgScore: number;
|
|
45
|
-
avgRating: number;
|
|
46
|
-
status: GapStatus;
|
|
47
|
-
resolution: string | null;
|
|
48
|
-
sampleIntents: string[];
|
|
49
|
-
resolvedAt?: string;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export type AddGapInput = {
|
|
53
|
-
intentCategory: string;
|
|
54
|
-
sampleCount?: number;
|
|
55
|
-
avgScore?: number;
|
|
56
|
-
avgRating?: number;
|
|
57
|
-
sampleIntents?: string[];
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Load all gaps from the registry file.
|
|
62
|
-
*/
|
|
63
|
-
export async function loadGaps(): Promise<Gap[]> {
|
|
64
|
-
if (!fs || !REGISTRY_PATH) return [];
|
|
65
|
-
try {
|
|
66
|
-
const content = await fs.readFile(REGISTRY_PATH, 'utf8');
|
|
67
|
-
return JSON.parse(content) as Gap[];
|
|
68
|
-
} catch {
|
|
69
|
-
return [];
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Save the full gaps array to disk.
|
|
75
|
-
*/
|
|
76
|
-
export async function saveGaps(gaps: Gap[]): Promise<void> {
|
|
77
|
-
if (!fs || !REGISTRY_PATH) return;
|
|
78
|
-
const dir = path!.dirname(REGISTRY_PATH);
|
|
79
|
-
await fs.mkdir(dir, { recursive: true });
|
|
80
|
-
await fs.writeFile(REGISTRY_PATH, JSON.stringify(gaps, null, 2) + '\n');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Add a new gap to the registry. Merges with existing if same intentCategory.
|
|
85
|
-
*/
|
|
86
|
-
export async function addGap(gap: AddGapInput): Promise<Gap[]> {
|
|
87
|
-
const gaps = await loadGaps();
|
|
88
|
-
const existing = gaps.find(g => g.intentCategory === gap.intentCategory && g.status !== 'resolved');
|
|
89
|
-
|
|
90
|
-
if (existing) {
|
|
91
|
-
// Merge: update stats, add new sample intents
|
|
92
|
-
existing.sampleCount = gap.sampleCount ?? existing.sampleCount;
|
|
93
|
-
existing.avgScore = gap.avgScore ?? existing.avgScore;
|
|
94
|
-
existing.avgRating = gap.avgRating ?? existing.avgRating;
|
|
95
|
-
existing.lastSeen = new Date().toISOString();
|
|
96
|
-
const intentSet = new Set([...existing.sampleIntents, ...(gap.sampleIntents ?? [])]);
|
|
97
|
-
existing.sampleIntents = [...intentSet].slice(0, 10);
|
|
98
|
-
} else {
|
|
99
|
-
gaps.push({
|
|
100
|
-
intentCategory: gap.intentCategory,
|
|
101
|
-
detectedAt: new Date().toISOString(),
|
|
102
|
-
lastSeen: new Date().toISOString(),
|
|
103
|
-
sampleCount: gap.sampleCount ?? 0,
|
|
104
|
-
avgScore: gap.avgScore ?? 0,
|
|
105
|
-
avgRating: gap.avgRating ?? 0,
|
|
106
|
-
status: 'open',
|
|
107
|
-
resolution: null,
|
|
108
|
-
sampleIntents: (gap.sampleIntents ?? []).slice(0, 10),
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
await saveGaps(gaps);
|
|
113
|
-
return gaps;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Update the status of a gap by intent category.
|
|
118
|
-
*/
|
|
119
|
-
export async function updateGapStatus(category: string, status: GapStatus, resolution?: string): Promise<Gap> {
|
|
120
|
-
const gaps = await loadGaps();
|
|
121
|
-
const gap = gaps.find(g => g.intentCategory === category && g.status !== 'resolved');
|
|
122
|
-
|
|
123
|
-
if (!gap) {
|
|
124
|
-
throw new Error(`No open gap found for category: ${category}`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
gap.status = status;
|
|
128
|
-
if (resolution) {
|
|
129
|
-
gap.resolution = resolution;
|
|
130
|
-
}
|
|
131
|
-
if (status === 'resolved') {
|
|
132
|
-
gap.resolvedAt = new Date().toISOString();
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
await saveGaps(gaps);
|
|
136
|
-
return gap;
|
|
137
|
-
}
|
package/feedback/index.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @adia-ai/a2ui-retrieval/feedback — feedback-loop surface.
|
|
3
|
-
*
|
|
4
|
-
* Pairs the feedback store, analyzer, dialog recorder, and gap registry
|
|
5
|
-
* — all the artifacts that compose the user-feedback ingestion loop.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export { feedbackStore } from './feedback-store.js';
|
|
9
|
-
export { FeedbackAnalyzer } from './feedback-analyzer.js';
|
|
10
|
-
export * from './feedback.js';
|
|
11
|
-
export { recordTurn, isRecording } from './dialog-recorder.js';
|
|
12
|
-
export type { TurnRecord } from './dialog-recorder.js';
|
|
13
|
-
export { loadGaps, addGap, updateGapStatus } from './gap-registry.js';
|
|
14
|
-
export type { Gap, GapStatus, AddGapInput } from './gap-registry.js';
|
package/intent/clarity.ts
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
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 { searchAll as searchCompositions } from '../../compose/strategies/zettel/composition-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
|
-
export type ClarityDimensions = {
|
|
59
|
-
domain: number;
|
|
60
|
-
scope: number;
|
|
61
|
-
content: number;
|
|
62
|
-
layout: number;
|
|
63
|
-
action: number;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export type ClarityQuestion = {
|
|
67
|
-
text: string;
|
|
68
|
-
dimension: string;
|
|
69
|
-
priority: number;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
export type ClarityResult = {
|
|
73
|
-
clear: boolean;
|
|
74
|
-
score: number;
|
|
75
|
-
dimensions: ClarityDimensions;
|
|
76
|
-
questions: ClarityQuestion[];
|
|
77
|
-
summary: string;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
type ClassificationArg = {
|
|
81
|
-
domain: string;
|
|
82
|
-
confidence: number;
|
|
83
|
-
matchedSignals: string[];
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Assess clarity of a user intent for UI generation.
|
|
88
|
-
*/
|
|
89
|
-
export function assessClarity(intent: string, classification?: ClassificationArg): ClarityResult {
|
|
90
|
-
const text = (intent ?? '').trim();
|
|
91
|
-
if (!text) {
|
|
92
|
-
return {
|
|
93
|
-
clear: false,
|
|
94
|
-
score: 0,
|
|
95
|
-
dimensions: { domain: 0, scope: 0, content: 0, layout: 0, action: 0 },
|
|
96
|
-
questions: [{ text: 'What would you like me to build?', dimension: 'domain', priority: 1 }],
|
|
97
|
-
summary: 'No intent provided',
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const cls = classification || classifyIntent(text);
|
|
102
|
-
const compositionMatches = searchCompositions(text);
|
|
103
|
-
const wordCount = text.split(/\s+/).length;
|
|
104
|
-
|
|
105
|
-
// ── Score each dimension ──
|
|
106
|
-
|
|
107
|
-
// Domain: how confident is the domain classification?
|
|
108
|
-
const domainScore = cls.confidence >= 0.3 ? 1 : cls.confidence >= 0.15 ? 0.6 : cls.matchedSignals.length > 0 ? 0.3 : 0;
|
|
109
|
-
|
|
110
|
-
// Scope: is the scope specific?
|
|
111
|
-
const scopeHits = SCOPE_SIGNALS.filter(r => r.test(text)).length;
|
|
112
|
-
const scopeScore = Math.min(1, scopeHits / 2);
|
|
113
|
-
|
|
114
|
-
// Content: are concrete labels/values mentioned?
|
|
115
|
-
const contentHits = CONTENT_SIGNALS.filter(r => r.test(text)).length;
|
|
116
|
-
const contentScore = Math.min(1, contentHits / 2);
|
|
117
|
-
|
|
118
|
-
// Layout: is a layout preference stated?
|
|
119
|
-
const layoutHits = LAYOUT_SIGNALS.filter(r => r.test(text)).length;
|
|
120
|
-
const layoutScore = Math.min(1, layoutHits);
|
|
121
|
-
|
|
122
|
-
// Action: are interactions mentioned?
|
|
123
|
-
const actionHits = ACTION_SIGNALS.filter(r => r.test(text)).length;
|
|
124
|
-
const actionScore = Math.min(1, actionHits / 2);
|
|
125
|
-
|
|
126
|
-
// ── Overall score (weighted) ──
|
|
127
|
-
const score = (
|
|
128
|
-
domainScore * 0.25 +
|
|
129
|
-
scopeScore * 0.25 +
|
|
130
|
-
contentScore * 0.20 +
|
|
131
|
-
layoutScore * 0.15 +
|
|
132
|
-
actionScore * 0.15
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
// ── Bonus: composition match adds confidence ──
|
|
136
|
-
const patternBonus = compositionMatches.length > 0 ? 0.15 : 0;
|
|
137
|
-
// Bonus: long intents are usually more specific
|
|
138
|
-
const lengthBonus = wordCount > 10 ? 0.1 : wordCount > 6 ? 0.05 : 0;
|
|
139
|
-
|
|
140
|
-
const finalScore = Math.min(1, score + patternBonus + lengthBonus);
|
|
141
|
-
|
|
142
|
-
// ── Generate targeted questions for weak dimensions ──
|
|
143
|
-
const questions: ClarityQuestion[] = [];
|
|
144
|
-
|
|
145
|
-
if (domainScore < 0.3) {
|
|
146
|
-
questions.push({
|
|
147
|
-
text: 'What type of UI is this? (e.g., a form, dashboard, profile card, settings page)',
|
|
148
|
-
dimension: 'domain',
|
|
149
|
-
priority: 1,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (scopeScore < 0.5) {
|
|
154
|
-
const domainQuestions: Record<string, string> = {
|
|
155
|
-
forms: 'What fields should the form include?',
|
|
156
|
-
data: 'What data should be displayed? How many items or metrics?',
|
|
157
|
-
layout: 'How many sections or cards should it have?',
|
|
158
|
-
agent: 'What actions should the assistant support?',
|
|
159
|
-
navigation: 'What pages or sections should be navigable?',
|
|
160
|
-
};
|
|
161
|
-
questions.push({
|
|
162
|
-
text: domainQuestions[cls.domain] ?? 'Can you be more specific about what it should contain?',
|
|
163
|
-
dimension: 'scope',
|
|
164
|
-
priority: 2,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (contentScore < 0.3 && scopeScore >= 0.3) {
|
|
169
|
-
const contentQuestions: Record<string, string> = {
|
|
170
|
-
forms: 'What labels should the fields have? (e.g., "Email", "Password", "Full Name")',
|
|
171
|
-
data: 'What metrics or values should be shown? (e.g., revenue, users, growth rate)',
|
|
172
|
-
layout: 'What content goes in each section?',
|
|
173
|
-
agent: 'What kind of messages or responses should be shown?',
|
|
174
|
-
navigation: 'What should the menu items or links be labeled?',
|
|
175
|
-
};
|
|
176
|
-
questions.push({
|
|
177
|
-
text: contentQuestions[cls.domain] ?? 'What specific content should be displayed?',
|
|
178
|
-
dimension: 'content',
|
|
179
|
-
priority: 3,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (layoutScore < 0.3 && scopeScore >= 0.5) {
|
|
184
|
-
questions.push({
|
|
185
|
-
text: 'Any layout preference? (e.g., grid of cards, single column, sidebar + content)',
|
|
186
|
-
dimension: 'layout',
|
|
187
|
-
priority: 4,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (actionScore < 0.3 && domainScore >= 0.3) {
|
|
192
|
-
questions.push({
|
|
193
|
-
text: 'What actions should users be able to take? (e.g., submit, edit, delete, filter)',
|
|
194
|
-
dimension: 'action',
|
|
195
|
-
priority: 5,
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Sort by priority, limit to 3
|
|
200
|
-
questions.sort((a, b) => a.priority - b.priority);
|
|
201
|
-
const topQuestions = questions.slice(0, 3);
|
|
202
|
-
|
|
203
|
-
// ── Determine if clear enough to proceed ──
|
|
204
|
-
// Clear only if dimensional score meets threshold
|
|
205
|
-
const clear = finalScore >= 0.4;
|
|
206
|
-
|
|
207
|
-
const summary = clear
|
|
208
|
-
? `Intent is ${finalScore >= 0.7 ? 'clear' : 'adequate'} (${Math.round(finalScore * 100)}%)`
|
|
209
|
-
: `Intent needs clarification (${Math.round(finalScore * 100)}% — below 40% threshold)`;
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
clear,
|
|
213
|
-
score: Math.round(finalScore * 100) / 100,
|
|
214
|
-
dimensions: {
|
|
215
|
-
domain: Math.round(domainScore * 100) / 100,
|
|
216
|
-
scope: Math.round(scopeScore * 100) / 100,
|
|
217
|
-
content: Math.round(contentScore * 100) / 100,
|
|
218
|
-
layout: Math.round(layoutScore * 100) / 100,
|
|
219
|
-
action: Math.round(actionScore * 100) / 100,
|
|
220
|
-
},
|
|
221
|
-
questions: topQuestions,
|
|
222
|
-
summary,
|
|
223
|
-
};
|
|
224
|
-
}
|