@widgetic/creator 0.3.72 → 0.3.73
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/dist/CreatorApp.svelte +821 -73
- package/dist/components/BrandKitPanel.svelte +556 -0
- package/dist/components/BrandKitPanel.svelte.d.ts +32 -0
- package/dist/components/DesignQuickEdit.svelte +218 -0
- package/dist/components/DesignQuickEdit.svelte.d.ts +33 -0
- package/dist/components/FileUrlSourceControl.svelte +69 -0
- package/dist/components/FileUrlSourceControl.svelte.d.ts +15 -0
- package/dist/components/VersionDropdownRow.svelte +86 -1
- package/dist/components/VersionDropdownRow.svelte.d.ts +10 -0
- package/dist/components/WidgetDetails.svelte +19 -2
- package/dist/components/WidgetShapeLivePreviews.svelte +8 -3
- package/dist/retiredSurface.test.d.ts +1 -0
- package/dist/retiredSurface.test.js +113 -0
- package/dist/stores/websocketStore.d.ts +3 -0
- package/dist/stores/websocketStore.js +4 -0
- package/dist/utils/canvasShapeSnapClient.d.ts +27 -0
- package/dist/utils/canvasShapeSnapClient.js +67 -0
- package/dist/utils/designIntentJevClient.d.ts +32 -0
- package/dist/utils/designIntentJevClient.js +51 -0
- package/dist/utils/designIntentResolver.d.ts +107 -0
- package/dist/utils/designIntentResolver.js +509 -0
- package/dist/utils/designIntentResolver.test.d.ts +1 -0
- package/dist/utils/designIntentResolver.test.js +246 -0
- package/dist/utils/designIntentTelemetry.d.ts +176 -0
- package/dist/utils/designIntentTelemetry.js +474 -0
- package/dist/utils/designIntentTelemetry.test.d.ts +1 -0
- package/dist/utils/designIntentTelemetry.test.js +385 -0
- package/dist/utils/designQuickEditValues.d.ts +51 -0
- package/dist/utils/designQuickEditValues.js +77 -0
- package/dist/utils/designQuickEditValues.test.d.ts +1 -0
- package/dist/utils/designQuickEditValues.test.js +100 -0
- package/dist/utils/pinScrollToBottom.js +9 -2
- package/package.json +2 -2
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* designIntentResolver — deterministic "user wants to change a design property"
|
|
3
|
+
* resolution, for the canvas chat.
|
|
4
|
+
*
|
|
5
|
+
* The canvas chat normally sends every message to codegen (an LLM turn). A large
|
|
6
|
+
* share of messages are really just design-property tweaks ("make the background
|
|
7
|
+
* #1a1a2e", "increase the padding by 8", "turn on the border"). Those are fully
|
|
8
|
+
* decidable in code against the widget's already-loaded `design.json` schema: the
|
|
9
|
+
* property set is a closed set, and the value is either explicit (hex / number /
|
|
10
|
+
* boolean / a dropdown option) or an arithmetic transform of the current value.
|
|
11
|
+
*
|
|
12
|
+
* This module is intentionally PURE and MODEL-FREE. It never calls JEV or an LLM;
|
|
13
|
+
* it returns `null` whenever the message needs semantic understanding ("make it
|
|
14
|
+
* pop", "give it more breathing room") so the caller can fall through to the
|
|
15
|
+
* existing codegen path. Arithmetic lives here, in code — never in the model.
|
|
16
|
+
*
|
|
17
|
+
* Scope: values that are unambiguous and safe to apply to the live preview:
|
|
18
|
+
* - colors (hex, rgb(), and a table of common named colors)
|
|
19
|
+
* - numbers (explicit, or delta/multiplier of the current value; clamped to constraints)
|
|
20
|
+
* - booleans (switch/checkbox/radiogroup on|off)
|
|
21
|
+
* - dropdown options (by option label/value token)
|
|
22
|
+
* - quoted free text (textInput/textArea) — only when explicitly quoted
|
|
23
|
+
*
|
|
24
|
+
* Anything else returns null, by design.
|
|
25
|
+
*/
|
|
26
|
+
const COLOR_CONTROLLERS = new Set(['colorpicker']);
|
|
27
|
+
const NUMBER_CONTROLLERS = new Set(['slider', 'number', 'angle']);
|
|
28
|
+
const BOOLEAN_CONTROLLERS = new Set(['switch', 'checkbox']);
|
|
29
|
+
const OPTION_CONTROLLERS = new Set(['dropdown', 'radiogroup']);
|
|
30
|
+
const TEXT_CONTROLLERS = new Set(['textinput', 'textarea']);
|
|
31
|
+
/** Common CSS named colors → hex. Deliberately a small, unambiguous subset. */
|
|
32
|
+
const NAMED_COLORS = {
|
|
33
|
+
black: '#000000',
|
|
34
|
+
white: '#ffffff',
|
|
35
|
+
red: '#ff0000',
|
|
36
|
+
green: '#008000',
|
|
37
|
+
blue: '#0000ff',
|
|
38
|
+
yellow: '#ffff00',
|
|
39
|
+
orange: '#ffa500',
|
|
40
|
+
purple: '#800080',
|
|
41
|
+
pink: '#ffc0cb',
|
|
42
|
+
brown: '#a52a2a',
|
|
43
|
+
grey: '#808080',
|
|
44
|
+
gray: '#808080',
|
|
45
|
+
navy: '#000080',
|
|
46
|
+
teal: '#008080',
|
|
47
|
+
cyan: '#00ffff',
|
|
48
|
+
aqua: '#00ffff',
|
|
49
|
+
magenta: '#ff00ff',
|
|
50
|
+
fuchsia: '#ff00ff',
|
|
51
|
+
lime: '#00ff00',
|
|
52
|
+
mint: '#98ff98',
|
|
53
|
+
beige: '#f5f5dc',
|
|
54
|
+
gold: '#ffd700',
|
|
55
|
+
silver: '#c0c0c0',
|
|
56
|
+
maroon: '#800000',
|
|
57
|
+
olive: '#808000',
|
|
58
|
+
coral: '#ff7f50',
|
|
59
|
+
salmon: '#fa8072',
|
|
60
|
+
turquoise: '#40e0d0',
|
|
61
|
+
lavender: '#e6e6fa',
|
|
62
|
+
indigo: '#4b0082',
|
|
63
|
+
crimson: '#dc143c',
|
|
64
|
+
transparent: '#00000000'
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Synonym expansion for property names. Keys are the words a user is likely to
|
|
68
|
+
* say; values are words that appear in schema property ids/labels. Matching is
|
|
69
|
+
* symmetric (a message token may be either side).
|
|
70
|
+
*/
|
|
71
|
+
const WORD_SYNONYMS = {
|
|
72
|
+
background: ['background', 'bg', 'backdrop'],
|
|
73
|
+
color: ['color', 'colour', 'tint', 'hue'],
|
|
74
|
+
radius: ['radius', 'corner', 'corners', 'rounded', 'roundness', 'curve'],
|
|
75
|
+
padding: ['padding', 'spacing', 'inset'],
|
|
76
|
+
margin: ['margin', 'gap', 'outer'],
|
|
77
|
+
font: ['font', 'typeface', 'type', 'typography'],
|
|
78
|
+
size: ['size', 'scale'],
|
|
79
|
+
border: ['border', 'stroke', 'outline', 'frame'],
|
|
80
|
+
width: ['width', 'thickness', 'weight'],
|
|
81
|
+
opacity: ['opacity', 'transparency', 'alpha'],
|
|
82
|
+
shadow: ['shadow', 'elevation', 'glow'],
|
|
83
|
+
spacing: ['spacing', 'gap', 'space'],
|
|
84
|
+
title: ['title', 'heading', 'header'],
|
|
85
|
+
text: ['text', 'label', 'copy', 'content'],
|
|
86
|
+
icon: ['icon', 'glyph'],
|
|
87
|
+
columns: ['columns', 'column', 'cols'],
|
|
88
|
+
layout: ['layout', 'arrangement'],
|
|
89
|
+
align: ['align', 'alignment'],
|
|
90
|
+
hover: ['hover'],
|
|
91
|
+
accent: ['accent', 'highlight', 'primary'],
|
|
92
|
+
secondary: ['secondary']
|
|
93
|
+
};
|
|
94
|
+
const INCREASE_VERBS = new Set([
|
|
95
|
+
'increase', 'bump', 'raise', 'grow', 'expand', 'add', 'more', 'bigger',
|
|
96
|
+
'larger', 'taller', 'wider', 'thicker', 'looser', 'up', 'boost', 'enlarge'
|
|
97
|
+
]);
|
|
98
|
+
const DECREASE_VERBS = new Set([
|
|
99
|
+
'decrease', 'reduce', 'lower', 'shrink', 'subtract', 'remove', 'less',
|
|
100
|
+
'smaller', 'shorter', 'narrower', 'thinner', 'tighter', 'down', 'cut', 'trim'
|
|
101
|
+
]);
|
|
102
|
+
/** Words that carry no property signal when they show up in a schema label. */
|
|
103
|
+
const STOPWORDS = new Set([
|
|
104
|
+
'the', 'and', 'for', 'with', 'your', 'you', 'this', 'that', 'from', 'into',
|
|
105
|
+
'widget', 'element', 'value', 'option', 'input', 'setting', 'settings', 'use',
|
|
106
|
+
'when', 'will', 'are', 'all', 'its', 'out', 'off', 'not', 'own'
|
|
107
|
+
]);
|
|
108
|
+
function splitIdentifier(raw) {
|
|
109
|
+
return String(raw || '')
|
|
110
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
111
|
+
.replace(/[_\-.]+/g, ' ')
|
|
112
|
+
.replace(/\s+/g, ' ')
|
|
113
|
+
.trim()
|
|
114
|
+
.toLowerCase()
|
|
115
|
+
.split(' ')
|
|
116
|
+
.filter(Boolean);
|
|
117
|
+
}
|
|
118
|
+
function normalizeHex(hex) {
|
|
119
|
+
let h = hex.replace('#', '').trim().toLowerCase();
|
|
120
|
+
if (h.length === 3)
|
|
121
|
+
h = h.split('').map((c) => c + c).join('');
|
|
122
|
+
return `#${h}`;
|
|
123
|
+
}
|
|
124
|
+
function rgbToHex(r, g, b) {
|
|
125
|
+
const to = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, '0');
|
|
126
|
+
return `#${to(r)}${to(g)}${to(b)}`;
|
|
127
|
+
}
|
|
128
|
+
function clampAndSnap(value, min, max, step) {
|
|
129
|
+
let v = value;
|
|
130
|
+
if (typeof min === 'number' && v < min)
|
|
131
|
+
v = min;
|
|
132
|
+
if (typeof max === 'number' && v > max)
|
|
133
|
+
v = max;
|
|
134
|
+
if (typeof step === 'number' && step > 0) {
|
|
135
|
+
const base = typeof min === 'number' ? min : 0;
|
|
136
|
+
v = base + Math.round((v - base) / step) * step;
|
|
137
|
+
// Round to a sane number of decimals for float steps.
|
|
138
|
+
v = Math.round(v * 1e6) / 1e6;
|
|
139
|
+
if (typeof min === 'number' && v < min)
|
|
140
|
+
v = min;
|
|
141
|
+
if (typeof max === 'number' && v > max)
|
|
142
|
+
v = max;
|
|
143
|
+
}
|
|
144
|
+
return v;
|
|
145
|
+
}
|
|
146
|
+
function toNumber(value) {
|
|
147
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
148
|
+
return value;
|
|
149
|
+
if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) {
|
|
150
|
+
return Number(value);
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
/** Flatten whatever schema shape the codegen produced into a flat property list. */
|
|
155
|
+
export function collectDesignProperties(schema) {
|
|
156
|
+
const out = [];
|
|
157
|
+
if (!schema)
|
|
158
|
+
return out;
|
|
159
|
+
const root = Array.isArray(schema)
|
|
160
|
+
? { componentGroups: schema }
|
|
161
|
+
: schema;
|
|
162
|
+
const groups = (root.componentGroups ?? root.components);
|
|
163
|
+
const groupList = Array.isArray(groups) ? groups : [];
|
|
164
|
+
const pushProperty = (raw) => {
|
|
165
|
+
if (!raw || typeof raw !== 'object')
|
|
166
|
+
return;
|
|
167
|
+
const p = raw;
|
|
168
|
+
const id = String(p.name ?? p.id ?? '').trim();
|
|
169
|
+
if (!id)
|
|
170
|
+
return;
|
|
171
|
+
const ic = (p.inputController ?? {});
|
|
172
|
+
const config = (ic.config ?? {});
|
|
173
|
+
const rawOptions = ic.options ?? config.options ?? p.options;
|
|
174
|
+
const options = Array.isArray(rawOptions)
|
|
175
|
+
? rawOptions
|
|
176
|
+
.map((o) => {
|
|
177
|
+
if (o && typeof o === 'object') {
|
|
178
|
+
const oo = o;
|
|
179
|
+
return { label: String(oo.label ?? oo.value ?? ''), value: String(oo.value ?? oo.label ?? '') };
|
|
180
|
+
}
|
|
181
|
+
return { label: String(o), value: String(o) };
|
|
182
|
+
})
|
|
183
|
+
.filter((o) => o.value !== '')
|
|
184
|
+
: [];
|
|
185
|
+
const constraints = (p.constraints ?? {});
|
|
186
|
+
const pickNum = (...vals) => {
|
|
187
|
+
for (const v of vals) {
|
|
188
|
+
const n = toNumber(v);
|
|
189
|
+
if (n !== null)
|
|
190
|
+
return n;
|
|
191
|
+
}
|
|
192
|
+
return undefined;
|
|
193
|
+
};
|
|
194
|
+
out.push({
|
|
195
|
+
id,
|
|
196
|
+
label: String(ic.description ?? ic.label ?? p.label ?? id),
|
|
197
|
+
controllerType: String(ic.type ?? p.type ?? '').toLowerCase(),
|
|
198
|
+
value: p.value ?? p.defaultValue,
|
|
199
|
+
defaultValue: p.defaultValue,
|
|
200
|
+
options,
|
|
201
|
+
min: pickNum(config.min, constraints.min),
|
|
202
|
+
max: pickNum(config.max, constraints.max),
|
|
203
|
+
step: pickNum(config.step, constraints.step),
|
|
204
|
+
helpText: typeof ic.helpText === 'string' ? ic.helpText : undefined
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
for (const group of groupList) {
|
|
208
|
+
if (!group || typeof group !== 'object')
|
|
209
|
+
continue;
|
|
210
|
+
const components = (group.components ?? []);
|
|
211
|
+
const componentList = Array.isArray(components) ? components : [];
|
|
212
|
+
for (const component of componentList) {
|
|
213
|
+
if (!component || typeof component !== 'object')
|
|
214
|
+
continue;
|
|
215
|
+
const c = component;
|
|
216
|
+
const direct = c.properties;
|
|
217
|
+
if (Array.isArray(direct))
|
|
218
|
+
direct.forEach(pushProperty);
|
|
219
|
+
const propertyGroups = c.propertyGroups;
|
|
220
|
+
if (Array.isArray(propertyGroups)) {
|
|
221
|
+
for (const pg of propertyGroups) {
|
|
222
|
+
const props = pg?.properties;
|
|
223
|
+
if (Array.isArray(props))
|
|
224
|
+
props.forEach(pushProperty);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
/** Extract a numeric operand + mode from the message (code side owns arithmetic). */
|
|
232
|
+
function extractNumberOperand(message) {
|
|
233
|
+
const text = message.toLowerCase();
|
|
234
|
+
// Direction verb, scanned over the whole message (word order is unreliable).
|
|
235
|
+
let sign = 0;
|
|
236
|
+
for (const word of text.match(/\b[a-z]+\b/g) ?? []) {
|
|
237
|
+
if (INCREASE_VERBS.has(word))
|
|
238
|
+
sign = 1;
|
|
239
|
+
else if (DECREASE_VERBS.has(word))
|
|
240
|
+
sign = -1;
|
|
241
|
+
}
|
|
242
|
+
const multiply = text.match(/\b(double|twice|triple|quadruple)\b/);
|
|
243
|
+
if (multiply) {
|
|
244
|
+
const factor = { double: 2, twice: 2, triple: 3, quadruple: 4 }[multiply[1]] ?? 2;
|
|
245
|
+
return { mode: 'multiply', value: factor, sign: 1 };
|
|
246
|
+
}
|
|
247
|
+
if (/\b(half|halve)\b/.test(text)) {
|
|
248
|
+
return { mode: 'divide', value: 2, sign: 1 };
|
|
249
|
+
}
|
|
250
|
+
// Percentage delta: "increase by 20%", "20% smaller", "reduce padding 25%".
|
|
251
|
+
const pct = text.match(/(\d+(?:\.\d+)?)\s*%/);
|
|
252
|
+
if (pct) {
|
|
253
|
+
return { mode: 'percent', value: Number(pct[1]), sign: sign === 0 ? 1 : sign };
|
|
254
|
+
}
|
|
255
|
+
// Absolute delta: a delta verb near a number ("increase padding by 8", "reduce 4").
|
|
256
|
+
const absDelta = text.match(/\b(increase|decrease|reduce|bump|raise|lower|grow|shrink|expand|add|subtract|remove|boost|trim|cut|more|less|bigger|smaller)\b[^0-9]{0,16}(\d+(?:\.\d+)?)/);
|
|
257
|
+
if (absDelta) {
|
|
258
|
+
const dir = INCREASE_VERBS.has(absDelta[1]) ? 1 : -1;
|
|
259
|
+
return { mode: 'delta', value: Number(absDelta[2]), sign: dir };
|
|
260
|
+
}
|
|
261
|
+
// Trailing delta: "8 more", "4 smaller".
|
|
262
|
+
const trailing = text.match(/(\d+(?:\.\d+)?)\s*(more|less|bigger|smaller)\b/);
|
|
263
|
+
if (trailing) {
|
|
264
|
+
return { mode: 'delta', value: Number(trailing[1]), sign: INCREASE_VERBS.has(trailing[2]) ? 1 : -1 };
|
|
265
|
+
}
|
|
266
|
+
// Signed delta: "+8" / "-4" (a hex color has no '+'/'-', so no collision).
|
|
267
|
+
const signed = text.match(/(?:^|[\s(])([+-])\s*(\d+(?:\.\d+)?)(?![\w.])/);
|
|
268
|
+
if (signed) {
|
|
269
|
+
return { mode: 'delta', value: Number(signed[2]), sign: signed[1] === '-' ? -1 : 1 };
|
|
270
|
+
}
|
|
271
|
+
// Explicit set: the first bare number.
|
|
272
|
+
const plain = text.match(/(\d+(?:\.\d+)?)/);
|
|
273
|
+
if (plain) {
|
|
274
|
+
return { mode: 'set', value: Number(plain[1]), sign: 1 };
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
function extractColor(message) {
|
|
279
|
+
const hex = message.match(/#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})\b/);
|
|
280
|
+
if (hex)
|
|
281
|
+
return { hex: normalizeHex(hex[0]), named: false };
|
|
282
|
+
const rgb = message.match(/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})/);
|
|
283
|
+
if (rgb)
|
|
284
|
+
return { hex: rgbToHex(Number(rgb[1]), Number(rgb[2]), Number(rgb[3])), named: false };
|
|
285
|
+
const words = message.toLowerCase().match(/\b[a-z]+\b/g) ?? [];
|
|
286
|
+
for (const w of words) {
|
|
287
|
+
if (NAMED_COLORS[w])
|
|
288
|
+
return { hex: NAMED_COLORS[w], named: true };
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
function extractQuotedText(message) {
|
|
293
|
+
const m = message.match(/["“”'‘’]([^"“”'‘’]{1,200})["“”'‘’]/);
|
|
294
|
+
return m ? m[1].trim() : null;
|
|
295
|
+
}
|
|
296
|
+
/** Score how strongly the message names this property. 0 means "not named". */
|
|
297
|
+
function nameMatchScore(message, property) {
|
|
298
|
+
const msgWords = new Set(message.toLowerCase().match(/[a-z]+/g) ?? []);
|
|
299
|
+
if (msgWords.size === 0)
|
|
300
|
+
return 0;
|
|
301
|
+
const propWords = new Set([
|
|
302
|
+
...splitIdentifier(property.id),
|
|
303
|
+
...splitIdentifier(property.label)
|
|
304
|
+
]);
|
|
305
|
+
let score = 0;
|
|
306
|
+
for (const pw of propWords) {
|
|
307
|
+
if (!pw || pw.length < 3 || STOPWORDS.has(pw))
|
|
308
|
+
continue;
|
|
309
|
+
// Direct word hit.
|
|
310
|
+
if (msgWords.has(pw)) {
|
|
311
|
+
score += 2;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
// Synonym hit (either direction).
|
|
315
|
+
const syns = WORD_SYNONYMS[pw] ?? [];
|
|
316
|
+
if (syns.some((s) => msgWords.has(s)))
|
|
317
|
+
score += 1;
|
|
318
|
+
}
|
|
319
|
+
// A whole-id substring hit is a strong signal ("backgroundcolor").
|
|
320
|
+
const compact = property.id.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
321
|
+
const msgCompact = message.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
322
|
+
if (compact.length >= 6 && msgCompact.includes(compact))
|
|
323
|
+
score += 3;
|
|
324
|
+
return score;
|
|
325
|
+
}
|
|
326
|
+
function booleanFromMessage(message) {
|
|
327
|
+
const text = message.toLowerCase();
|
|
328
|
+
const flip = /\b(turn|switch|set)\b/.test(text);
|
|
329
|
+
const hasOn = /\b(enable[ds]?|show|visible|true|yes|add)\b/.test(text) || (flip && /\bon\b/.test(text));
|
|
330
|
+
const hasOff = /\b(disable[ds]?|hide|hidden|false|no|remove)\b/.test(text) || (flip && /\boff\b/.test(text));
|
|
331
|
+
if (hasOn && hasOff)
|
|
332
|
+
return null;
|
|
333
|
+
if (hasOn)
|
|
334
|
+
return true;
|
|
335
|
+
if (hasOff)
|
|
336
|
+
return false;
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
function buildCandidate(property, message) {
|
|
340
|
+
const type = property.controllerType;
|
|
341
|
+
const current = property.value !== undefined ? property.value : property.defaultValue;
|
|
342
|
+
if (COLOR_CONTROLLERS.has(type)) {
|
|
343
|
+
const color = extractColor(message);
|
|
344
|
+
if (color)
|
|
345
|
+
return { property, value: color.hex, reason: color.named ? 'named-color' : 'explicit-color' };
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
if (NUMBER_CONTROLLERS.has(type)) {
|
|
349
|
+
const operand = extractNumberOperand(message);
|
|
350
|
+
if (!operand)
|
|
351
|
+
return null;
|
|
352
|
+
if (operand.mode === 'set') {
|
|
353
|
+
return {
|
|
354
|
+
property,
|
|
355
|
+
value: clampAndSnap(operand.value, property.min, property.max, property.step),
|
|
356
|
+
reason: 'explicit-number'
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const cur = toNumber(current);
|
|
360
|
+
if (cur === null)
|
|
361
|
+
return null;
|
|
362
|
+
let next;
|
|
363
|
+
if (operand.mode === 'multiply')
|
|
364
|
+
next = cur * operand.value;
|
|
365
|
+
else if (operand.mode === 'divide')
|
|
366
|
+
next = cur / operand.value;
|
|
367
|
+
else if (operand.mode === 'percent')
|
|
368
|
+
next = cur * (1 + (operand.sign * operand.value) / 100);
|
|
369
|
+
else
|
|
370
|
+
next = cur + operand.sign * operand.value;
|
|
371
|
+
return {
|
|
372
|
+
property,
|
|
373
|
+
value: clampAndSnap(next, property.min, property.max, property.step),
|
|
374
|
+
reason: 'delta-number'
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (BOOLEAN_CONTROLLERS.has(type)) {
|
|
378
|
+
const b = booleanFromMessage(message);
|
|
379
|
+
if (b === null)
|
|
380
|
+
return null;
|
|
381
|
+
return { property, value: b, reason: 'boolean' };
|
|
382
|
+
}
|
|
383
|
+
if (OPTION_CONTROLLERS.has(type)) {
|
|
384
|
+
const msgWords = new Set(message.toLowerCase().match(/[a-z0-9]+/g) ?? []);
|
|
385
|
+
for (const opt of property.options) {
|
|
386
|
+
const optWords = [
|
|
387
|
+
...splitIdentifier(opt.label),
|
|
388
|
+
...splitIdentifier(opt.value)
|
|
389
|
+
].filter((w) => w.length >= 3);
|
|
390
|
+
if (optWords.length > 0 && optWords.every((w) => msgWords.has(w))) {
|
|
391
|
+
return { property, value: opt.value, reason: 'option' };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
if (TEXT_CONTROLLERS.has(type)) {
|
|
397
|
+
const text = extractQuotedText(message);
|
|
398
|
+
if (text)
|
|
399
|
+
return { property, value: text, reason: 'explicit-text' };
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Properties the message names (directly or by synonym), strongest first.
|
|
406
|
+
* Used by the shadow-mode telemetry classifier to bucket a command without
|
|
407
|
+
* resolving a value for it.
|
|
408
|
+
*/
|
|
409
|
+
export function scoreDesignProperties(message, schema) {
|
|
410
|
+
const raw = String(message ?? '').trim();
|
|
411
|
+
return collectDesignProperties(schema)
|
|
412
|
+
.map((property) => ({ property, score: nameMatchScore(raw, property) }))
|
|
413
|
+
.filter((entry) => entry.score > 0)
|
|
414
|
+
.sort((a, b) => b.score - a.score);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Resolve a chat message into concrete design-property changes, deterministically.
|
|
418
|
+
* Returns `null` when the message is not an unambiguous, in-scope design tweak —
|
|
419
|
+
* the caller should then fall through to the existing codegen path.
|
|
420
|
+
*/
|
|
421
|
+
export function resolveDesignIntent(message, schema, currentValues) {
|
|
422
|
+
const raw = String(message ?? '').trim();
|
|
423
|
+
if (!raw || raw.length > 400)
|
|
424
|
+
return null;
|
|
425
|
+
const properties = collectDesignProperties(schema);
|
|
426
|
+
if (properties.length === 0)
|
|
427
|
+
return null;
|
|
428
|
+
const scored = [];
|
|
429
|
+
for (const property of properties) {
|
|
430
|
+
const withScore = buildCandidate(property, raw);
|
|
431
|
+
if (!withScore)
|
|
432
|
+
continue;
|
|
433
|
+
const score = nameMatchScore(raw, property);
|
|
434
|
+
if (score <= 0)
|
|
435
|
+
continue;
|
|
436
|
+
scored.push({ ...withScore, score });
|
|
437
|
+
}
|
|
438
|
+
if (scored.length === 0)
|
|
439
|
+
return null;
|
|
440
|
+
// Deterministic ordering: score desc, then schema order for stability.
|
|
441
|
+
scored.sort((a, b) => b.score - a.score);
|
|
442
|
+
// Ambiguity guard: a tie between two different, equally-named properties means
|
|
443
|
+
// we cannot be sure which one the user meant — bail to the codegen path.
|
|
444
|
+
const top = scored.filter((c) => c.score === scored[0].score);
|
|
445
|
+
const distinctIds = new Set(top.map((c) => c.property.id));
|
|
446
|
+
if (distinctIds.size > 1)
|
|
447
|
+
return null;
|
|
448
|
+
const winner = scored[0];
|
|
449
|
+
// If the caller passed live current values, prefer them over the schema default.
|
|
450
|
+
if (currentValues && Object.prototype.hasOwnProperty.call(currentValues, winner.property.id)) {
|
|
451
|
+
winner.property = { ...winner.property, value: currentValues[winner.property.id] };
|
|
452
|
+
}
|
|
453
|
+
const previousValue = winner.property.value;
|
|
454
|
+
const change = {
|
|
455
|
+
propertyId: winner.property.id,
|
|
456
|
+
label: winner.property.label,
|
|
457
|
+
controllerType: winner.property.controllerType,
|
|
458
|
+
previousValue,
|
|
459
|
+
value: winner.value,
|
|
460
|
+
reason: winner.reason
|
|
461
|
+
};
|
|
462
|
+
return {
|
|
463
|
+
changes: [change],
|
|
464
|
+
summary: buildSummary(change)
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function formatValue(value) {
|
|
468
|
+
if (typeof value === 'boolean')
|
|
469
|
+
return value ? 'on' : 'off';
|
|
470
|
+
if (value === null || value === undefined)
|
|
471
|
+
return '—';
|
|
472
|
+
return String(value);
|
|
473
|
+
}
|
|
474
|
+
function buildSummary(change) {
|
|
475
|
+
const verb = {
|
|
476
|
+
'explicit-color': 'Set',
|
|
477
|
+
'named-color': 'Set',
|
|
478
|
+
'explicit-number': 'Set',
|
|
479
|
+
'delta-number': 'Adjusted',
|
|
480
|
+
boolean: 'Turned',
|
|
481
|
+
option: 'Changed',
|
|
482
|
+
'explicit-text': 'Set'
|
|
483
|
+
};
|
|
484
|
+
return `${verb[change.reason]} ${change.label} to ${formatValue(change.value)}.`;
|
|
485
|
+
}
|
|
486
|
+
/** Build the chat card payload from a resolution (used to render inline controls). */
|
|
487
|
+
export function toDesignControlRequest(resolution, schema, _currentValues, compositionId) {
|
|
488
|
+
const byId = new Map(collectDesignProperties(schema).map((p) => [p.id, p]));
|
|
489
|
+
const properties = resolution.changes.map((change) => {
|
|
490
|
+
const schemaProp = byId.get(change.propertyId);
|
|
491
|
+
return {
|
|
492
|
+
id: change.propertyId,
|
|
493
|
+
label: change.label,
|
|
494
|
+
controllerType: change.controllerType,
|
|
495
|
+
// The value the change resolved to — the card shows what was just applied.
|
|
496
|
+
value: change.value,
|
|
497
|
+
options: schemaProp?.options,
|
|
498
|
+
min: schemaProp?.min,
|
|
499
|
+
max: schemaProp?.max,
|
|
500
|
+
step: schemaProp?.step,
|
|
501
|
+
helpText: schemaProp?.helpText
|
|
502
|
+
};
|
|
503
|
+
});
|
|
504
|
+
return {
|
|
505
|
+
summary: resolution.summary,
|
|
506
|
+
properties,
|
|
507
|
+
...(compositionId ? { compositionId } : {})
|
|
508
|
+
};
|
|
509
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|