@widgetic/creator 0.3.71 → 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.
Files changed (33) hide show
  1. package/dist/CreatorApp.svelte +882 -78
  2. package/dist/components/BrandKitPanel.svelte +556 -0
  3. package/dist/components/BrandKitPanel.svelte.d.ts +32 -0
  4. package/dist/components/DesignQuickEdit.svelte +218 -0
  5. package/dist/components/DesignQuickEdit.svelte.d.ts +33 -0
  6. package/dist/components/FileUrlSourceControl.svelte +69 -0
  7. package/dist/components/FileUrlSourceControl.svelte.d.ts +15 -0
  8. package/dist/components/VersionDropdownRow.svelte +86 -1
  9. package/dist/components/VersionDropdownRow.svelte.d.ts +10 -0
  10. package/dist/components/WidgetDetails.svelte +19 -2
  11. package/dist/components/WidgetShapeLivePreviews.svelte +8 -3
  12. package/dist/retiredSurface.test.d.ts +1 -0
  13. package/dist/retiredSurface.test.js +113 -0
  14. package/dist/stores/websocketStore.d.ts +3 -0
  15. package/dist/stores/websocketStore.js +4 -0
  16. package/dist/utils/canvasShapeSnapClient.d.ts +27 -0
  17. package/dist/utils/canvasShapeSnapClient.js +67 -0
  18. package/dist/utils/designIntentJevClient.d.ts +32 -0
  19. package/dist/utils/designIntentJevClient.js +51 -0
  20. package/dist/utils/designIntentResolver.d.ts +107 -0
  21. package/dist/utils/designIntentResolver.js +509 -0
  22. package/dist/utils/designIntentResolver.test.d.ts +1 -0
  23. package/dist/utils/designIntentResolver.test.js +246 -0
  24. package/dist/utils/designIntentTelemetry.d.ts +176 -0
  25. package/dist/utils/designIntentTelemetry.js +474 -0
  26. package/dist/utils/designIntentTelemetry.test.d.ts +1 -0
  27. package/dist/utils/designIntentTelemetry.test.js +385 -0
  28. package/dist/utils/designQuickEditValues.d.ts +51 -0
  29. package/dist/utils/designQuickEditValues.js +77 -0
  30. package/dist/utils/designQuickEditValues.test.d.ts +1 -0
  31. package/dist/utils/designQuickEditValues.test.js +100 -0
  32. package/dist/utils/pinScrollToBottom.js +9 -2
  33. package/package.json +2 -2
@@ -0,0 +1,246 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { collectDesignProperties, resolveDesignIntent, toDesignControlRequest } from './designIntentResolver';
3
+ /**
4
+ * A widget-shaped design schema (the post-codegen shape: one componentGroup,
5
+ * components carrying `properties`, options under `inputController.config`).
6
+ */
7
+ const SCHEMA = {
8
+ componentGroups: [
9
+ {
10
+ name: 'Design',
11
+ components: [
12
+ {
13
+ id: 'appearance',
14
+ type: 'layoutGroup',
15
+ propertyGroups: [
16
+ {
17
+ name: 'Colors',
18
+ properties: [
19
+ {
20
+ id: 'backgroundColor',
21
+ type: 'string',
22
+ defaultValue: '#ffffff',
23
+ inputController: { type: 'colorPicker', description: 'Background Color' }
24
+ },
25
+ {
26
+ id: 'titleColor',
27
+ type: 'string',
28
+ defaultValue: '#111827',
29
+ inputController: { type: 'colorPicker', description: 'Title Color' }
30
+ },
31
+ {
32
+ id: 'widgetBorderColor',
33
+ type: 'string',
34
+ defaultValue: '#e5e7eb',
35
+ inputController: { type: 'colorPicker', description: 'Border Color' }
36
+ }
37
+ ]
38
+ },
39
+ {
40
+ name: 'Spacing',
41
+ properties: [
42
+ {
43
+ id: 'padding',
44
+ type: 'number',
45
+ defaultValue: 16,
46
+ inputController: { type: 'slider', description: 'Padding', config: { min: 0, max: 64, step: 1 } }
47
+ },
48
+ {
49
+ id: 'borderRadius',
50
+ type: 'number',
51
+ defaultValue: 8,
52
+ inputController: { type: 'number', description: 'Border Radius', config: { min: 0, max: 40, step: 1 } }
53
+ }
54
+ ]
55
+ },
56
+ {
57
+ name: 'Layout',
58
+ properties: [
59
+ {
60
+ id: 'layout',
61
+ type: 'string',
62
+ defaultValue: 'grid',
63
+ inputController: {
64
+ type: 'dropdown',
65
+ description: 'Layout Style',
66
+ config: {
67
+ options: [
68
+ { label: 'Grid', value: 'grid' },
69
+ { label: 'List', value: 'list' },
70
+ { label: 'Carousel', value: 'carousel' }
71
+ ]
72
+ }
73
+ }
74
+ },
75
+ {
76
+ id: 'showShadow',
77
+ type: 'boolean',
78
+ defaultValue: true,
79
+ inputController: { type: 'switch', description: 'Drop Shadow' }
80
+ },
81
+ {
82
+ id: 'widgetTitle',
83
+ type: 'string',
84
+ defaultValue: 'My Widget',
85
+ inputController: { type: 'textInput', description: 'Widget Title' }
86
+ }
87
+ ]
88
+ }
89
+ ]
90
+ }
91
+ ]
92
+ }
93
+ ]
94
+ };
95
+ const LIVE = { backgroundColor: '#ffffff', padding: 16, borderRadius: 8, showShadow: true };
96
+ describe('collectDesignProperties', () => {
97
+ it('flattens nested property groups into descriptors', () => {
98
+ const props = collectDesignProperties(SCHEMA);
99
+ expect(props.map((p) => p.id)).toEqual([
100
+ 'backgroundColor',
101
+ 'titleColor',
102
+ 'widgetBorderColor',
103
+ 'padding',
104
+ 'borderRadius',
105
+ 'layout',
106
+ 'showShadow',
107
+ 'widgetTitle'
108
+ ]);
109
+ });
110
+ it('reads dropdown options from inputController.config.options', () => {
111
+ const dropdown = collectDesignProperties(SCHEMA).find((p) => p.id === 'layout');
112
+ expect(dropdown?.options).toHaveLength(3);
113
+ expect(dropdown?.options[1]).toEqual({ label: 'List', value: 'list' });
114
+ });
115
+ it('reads numeric constraints', () => {
116
+ const padding = collectDesignProperties(SCHEMA).find((p) => p.id === 'padding');
117
+ expect(padding).toMatchObject({ min: 0, max: 64, step: 1 });
118
+ });
119
+ });
120
+ describe('resolveDesignIntent — colors', () => {
121
+ it('resolves an explicit hex on the named color property', () => {
122
+ const r = resolveDesignIntent('make the background #1a1a2e', SCHEMA, LIVE);
123
+ expect(r?.changes).toHaveLength(1);
124
+ expect(r?.changes[0]).toMatchObject({
125
+ propertyId: 'backgroundColor',
126
+ value: '#1a1a2e',
127
+ reason: 'explicit-color'
128
+ });
129
+ });
130
+ it('expands 3-digit hex', () => {
131
+ const r = resolveDesignIntent('set the background to #abc', SCHEMA, LIVE);
132
+ expect(r?.changes[0].value).toBe('#aabbcc');
133
+ });
134
+ it('resolves a named color', () => {
135
+ const r = resolveDesignIntent('change the background to red', SCHEMA, LIVE);
136
+ expect(r?.changes[0]).toMatchObject({ propertyId: 'backgroundColor', value: '#ff0000', reason: 'named-color' });
137
+ });
138
+ it('resolves rgb()', () => {
139
+ const r = resolveDesignIntent('background rgb(26, 26, 46)', SCHEMA, LIVE);
140
+ expect(r?.changes[0].value).toBe('#1a1a2e');
141
+ });
142
+ it('does not touch a number property when only a color is given', () => {
143
+ const r = resolveDesignIntent('make the padding red', SCHEMA, LIVE);
144
+ expect(r).toBeNull();
145
+ });
146
+ });
147
+ describe('resolveDesignIntent — numbers (arithmetic stays in code)', () => {
148
+ it('sets an explicit number', () => {
149
+ const r = resolveDesignIntent('set the border radius to 12', SCHEMA, LIVE);
150
+ expect(r?.changes[0]).toMatchObject({ propertyId: 'borderRadius', value: 12, reason: 'explicit-number' });
151
+ });
152
+ it('increases by an absolute delta using the current value', () => {
153
+ const r = resolveDesignIntent('increase the padding by 8', SCHEMA, LIVE);
154
+ expect(r?.changes[0]).toMatchObject({ propertyId: 'padding', value: 24, reason: 'delta-number' });
155
+ });
156
+ it('applies a percentage delta', () => {
157
+ const r = resolveDesignIntent('reduce the padding by 25%', SCHEMA, LIVE);
158
+ expect(r?.changes[0].value).toBe(12);
159
+ });
160
+ it('doubles the current value', () => {
161
+ const r = resolveDesignIntent('double the border radius', SCHEMA, LIVE);
162
+ expect(r?.changes[0].value).toBe(16);
163
+ });
164
+ it('halves the current value', () => {
165
+ const r = resolveDesignIntent('half the padding', SCHEMA, LIVE);
166
+ expect(r?.changes[0].value).toBe(8);
167
+ });
168
+ it('clamps to the schema max', () => {
169
+ const r = resolveDesignIntent('increase the padding by 500', SCHEMA, LIVE);
170
+ expect(r?.changes[0].value).toBe(64);
171
+ });
172
+ it('uses the schema default when no live value is supplied', () => {
173
+ const r = resolveDesignIntent('increase the padding by 4', SCHEMA);
174
+ expect(r?.changes[0]).toMatchObject({ previousValue: 16, value: 20 });
175
+ });
176
+ });
177
+ describe('resolveDesignIntent — booleans and options', () => {
178
+ it('turns a switch on', () => {
179
+ const r = resolveDesignIntent('turn the drop shadow off', SCHEMA, LIVE);
180
+ expect(r?.changes[0]).toMatchObject({ propertyId: 'showShadow', value: false, reason: 'boolean' });
181
+ });
182
+ it('enables', () => {
183
+ const r = resolveDesignIntent('enable the shadow', SCHEMA, LIVE);
184
+ expect(r?.changes[0]).toMatchObject({ propertyId: 'showShadow', value: true });
185
+ });
186
+ it('selects a dropdown option by label', () => {
187
+ const r = resolveDesignIntent('switch the layout to carousel', SCHEMA, LIVE);
188
+ expect(r?.changes[0]).toMatchObject({ propertyId: 'layout', value: 'carousel', reason: 'option' });
189
+ });
190
+ });
191
+ describe('resolveDesignIntent — text', () => {
192
+ it('resolves only explicitly quoted text', () => {
193
+ const r = resolveDesignIntent('set the widget title to "Grocery list"', SCHEMA, LIVE);
194
+ expect(r?.changes[0]).toMatchObject({ propertyId: 'widgetTitle', value: 'Grocery list' });
195
+ });
196
+ it('refuses unquoted text (falls through to codegen)', () => {
197
+ expect(resolveDesignIntent('change the title to grocery list', SCHEMA, LIVE)).toBeNull();
198
+ });
199
+ });
200
+ describe('resolveDesignIntent — bails out (falls through to codegen/JEV)', () => {
201
+ it('returns null for vague intent', () => {
202
+ expect(resolveDesignIntent('make it pop', SCHEMA, LIVE)).toBeNull();
203
+ expect(resolveDesignIntent('give it more breathing room', SCHEMA, LIVE)).toBeNull();
204
+ expect(resolveDesignIntent('make it feel more premium', SCHEMA, LIVE)).toBeNull();
205
+ });
206
+ it('returns null for a color with no named property', () => {
207
+ expect(resolveDesignIntent('make it red', SCHEMA, LIVE)).toBeNull();
208
+ });
209
+ it('returns null for a non-design ask', () => {
210
+ expect(resolveDesignIntent('add a confetti animation when the timer ends', SCHEMA, LIVE)).toBeNull();
211
+ });
212
+ it('returns null when two properties tie on name match', () => {
213
+ // "color" alone names all three color properties equally → ambiguous.
214
+ expect(resolveDesignIntent('change the color to red', SCHEMA, LIVE)).toBeNull();
215
+ });
216
+ it('returns null for an empty schema', () => {
217
+ expect(resolveDesignIntent('make the background red', {}, LIVE)).toBeNull();
218
+ });
219
+ });
220
+ describe('toDesignControlRequest', () => {
221
+ it('carries the resolved value + constraints for the inline controller', () => {
222
+ const r = resolveDesignIntent('increase the padding by 8', SCHEMA, LIVE);
223
+ const card = toDesignControlRequest(r, SCHEMA, LIVE);
224
+ expect(card.summary).toContain('Padding');
225
+ expect(card.properties[0]).toMatchObject({
226
+ id: 'padding',
227
+ controllerType: 'slider',
228
+ value: 24,
229
+ min: 0,
230
+ max: 64
231
+ });
232
+ });
233
+ it('records the composition the change landed in, so a reload can re-attach it', () => {
234
+ const r = resolveDesignIntent('increase the padding by 8', SCHEMA, LIVE);
235
+ expect(toDesignControlRequest(r, SCHEMA, LIVE, 'composition-1').compositionId).toBe('composition-1');
236
+ });
237
+ it('omits the composition rather than recording null when there is none', () => {
238
+ // A card persisted before the field existed, and one authored against a
239
+ // composition that was never saved, both arrive without it — and the host
240
+ // treats exactly that shape as "attached".
241
+ const r = resolveDesignIntent('increase the padding by 8', SCHEMA, LIVE);
242
+ expect(toDesignControlRequest(r, SCHEMA, LIVE)).not.toHaveProperty('compositionId');
243
+ expect(toDesignControlRequest(r, SCHEMA, LIVE, null)).not.toHaveProperty('compositionId');
244
+ expect(toDesignControlRequest(r, SCHEMA, LIVE, '')).not.toHaveProperty('compositionId');
245
+ });
246
+ });
@@ -0,0 +1,176 @@
1
+ /**
2
+ * designIntentTelemetry — shadow-mode measurement for the canvas chat.
3
+ *
4
+ * Every message a user sends in the canvas chat is bucketed by *how it could be
5
+ * resolved*, independently of what the app actually does:
6
+ *
7
+ * literal_arithmetic — explicit value / arithmetic in code (the deterministic
8
+ * resolver already handles it; no model needed)
9
+ * bounded_set_semantic — a design ask whose property/value needs judgment
10
+ * (the JEV-shaped residual: Choice over the closed set)
11
+ * free_text — needs a text value (an LLM)
12
+ * new_property — asks for a design property that does not exist yet
13
+ * (generation; an LLM)
14
+ * not_design — not a design tweak at all → codegen as today
15
+ *
16
+ * This module is measurement only: it never applies a change and never alters a
17
+ * send. It records one event per message, persists rolling counts so fractions
18
+ * survive reloads, and reports them. Enabling it is safe in production.
19
+ *
20
+ * The Jev fallback is measured twice over: how big its candidate pool is (the
21
+ * bounded-set-semantic bucket) and how much of that pool it resolves. Every dial
22
+ * is attributed to the *ask shape* that produced it, so a fall-through count can
23
+ * be read against the question it implicates — a weak value bucket and a weak
24
+ * property roster produce entirely different reasons — instead of as one global
25
+ * total.
26
+ */
27
+ export type DesignIntentBucket = 'literal_arithmetic' | 'bounded_set_semantic' | 'free_text' | 'new_property' | 'not_design';
28
+ export declare const DESIGN_INTENT_BUCKETS: DesignIntentBucket[];
29
+ /**
30
+ * The two shapes a bounded-set-semantic ask arrives in. Which one it is decides
31
+ * what a fall-through implicates, because a different question carries the
32
+ * weight in each:
33
+ *
34
+ * property_name — the message named a property, so the **value** question is
35
+ * under test: `low_confidence_value` / `no_value_stated` point
36
+ * at the value option strings.
37
+ * value_only — only a value was stated and which property it targets needs
38
+ * judgment, so the **property** question is: `low_confidence`
39
+ * / `none_of_these` point at the property option strings or
40
+ * the confidence floor.
41
+ */
42
+ export type DesignIntentAskShape = 'property_named' | 'value_only';
43
+ /** Every ask shape, plus dials that outlived their classification (a refresh
44
+ * mid-conversation, or counts persisted before shapes were recorded). */
45
+ export type DesignIntentJevShape = DesignIntentAskShape | 'unknown';
46
+ export declare const JE_ASK_SHAPES: DesignIntentJevShape[];
47
+ export interface DesignIntentClassification {
48
+ bucket: DesignIntentBucket;
49
+ /** Design properties the message named (empty when nothing matched). */
50
+ propertyIds: string[];
51
+ detail: string;
52
+ /** Set on the bounded-set-semantic bucket only — the shape is what Jev's
53
+ * reason counts are read against. */
54
+ shape?: DesignIntentAskShape;
55
+ }
56
+ /**
57
+ * Bucket a single chat message. Pure and deterministic — the same heuristics the
58
+ * resolver uses for property naming, plus a few value/verb signals.
59
+ */
60
+ export declare function classifyDesignIntent(message: string, schema: unknown): DesignIntentClassification;
61
+ export interface DesignIntentTelemetryEvent extends DesignIntentClassification {
62
+ messageLength: number;
63
+ at: number;
64
+ }
65
+ /** One ask shape's slice of the Jev dials. */
66
+ export interface DesignIntentJevShapeBreakdown {
67
+ attempts: number;
68
+ acted: number;
69
+ fellThrough: number;
70
+ /** `acted / attempts`, 0 when this shape was never dialed. */
71
+ resolvedShare: number;
72
+ byReason: Record<string, number>;
73
+ }
74
+ export interface DesignIntentJevSnapshot {
75
+ /** Jev dials attempted on the bounded-set-semantic bucket. */
76
+ attempts: number;
77
+ /** Dials whose verdict was applied (enforce mode + confident + bounded value). */
78
+ acted: number;
79
+ /** Dials that fell through to codegen, for any reason. */
80
+ fellThrough: number;
81
+ /** `acted / attempts`, 0 when nothing was attempted. */
82
+ resolvedShare: number;
83
+ /** Fall-through (or acted) reasons, for tuning the questions and the gates. */
84
+ byReason: Record<string, number>;
85
+ /** The same counters split by ask shape, so a reason count can be read against
86
+ * the question that produced it rather than as a global total. */
87
+ byShape: Record<DesignIntentJevShape, DesignIntentJevShapeBreakdown>;
88
+ /** Dials attributed to a bucket other than the one the design says is ever
89
+ * dialed. Should stay 0: a non-zero value means a dial is happening where the
90
+ * module claims it cannot, so it is counted rather than folded in. */
91
+ offBucketDials: number;
92
+ }
93
+ export interface DesignIntentTelemetrySnapshot {
94
+ /** Every recorded send, including ones with no design schema loaded. */
95
+ total: number;
96
+ /** Sends that could be classified (a design schema was loaded). */
97
+ counted: number;
98
+ /** Sends recorded while no design schema was available (unclassifiable). */
99
+ noSchema: number;
100
+ buckets: Record<DesignIntentBucket, number>;
101
+ /** Share of `counted` per bucket, 0–1. */
102
+ fractions: Record<DesignIntentBucket, number>;
103
+ /** Share of `counted` the deterministic path could have applied. */
104
+ deterministicShare: number;
105
+ /** Share of `counted` that is a design ask needing a model (JEV or LLM). */
106
+ modelShare: number;
107
+ /** How the Jev fallback fared on its one candidate bucket. */
108
+ jev: DesignIntentJevSnapshot;
109
+ }
110
+ /**
111
+ * The one bucket Jev is asked about. It is the residual where a design property
112
+ * or value needs judgment but the value space is closed — `free_text` and
113
+ * `new_property` can only fall through (Jev cannot generate), and `not_design`
114
+ * is not a design ask at all, so neither is ever dialed.
115
+ */
116
+ export declare const JEV_CANDIDATE_BUCKET: DesignIntentBucket;
117
+ export declare function isJevCandidateBucket(bucket: DesignIntentBucket | null | undefined): boolean;
118
+ export type DesignIntentJevSource = 'jev' | 'degraded' | 'disabled' | 'no_properties' | 'client_unavailable';
119
+ /** The on-disk shape of one ask shape's counters — exported because loading it
120
+ * is a migration, and a migration is worth asserting on directly. */
121
+ export interface PersistedShapeBreakdown {
122
+ attempts: number;
123
+ acted: number;
124
+ fellThrough: number;
125
+ byReason: Record<string, number>;
126
+ }
127
+ /** The persisted Jev counters. `byShape` and `offBucketDials` were added after
128
+ * the first version of this payload shipped, so a stored payload from before
129
+ * them must still load — see `parsePersistedTelemetry`. */
130
+ export interface PersistedJev {
131
+ attempts: number;
132
+ acted: number;
133
+ fellThrough: number;
134
+ byReason: Record<string, number>;
135
+ byShape: Record<DesignIntentJevShape, PersistedShapeBreakdown>;
136
+ offBucketDials: number;
137
+ }
138
+ /** The whole persisted payload, as stored under `STORAGE_KEY`. */
139
+ export interface PersistedDesignIntentTelemetry {
140
+ total: number;
141
+ noSchema: number;
142
+ buckets: Record<DesignIntentBucket, number>;
143
+ jev: PersistedJev;
144
+ }
145
+ /**
146
+ * Parse one persisted payload. Pure, so the upgrade path — every shape recorded
147
+ * before `byShape` existed — is testable without a browser: counts that are
148
+ * absent, negative or not numbers become zero, and the flat reason map it does
149
+ * have is kept.
150
+ */
151
+ export declare function parsePersistedTelemetry(raw: string | null): PersistedDesignIntentTelemetry;
152
+ /**
153
+ * Record one canvas-chat send and bucket it. Shadow mode: this does not change
154
+ * any behavior — the caller generates exactly as it did before. Returns the
155
+ * classification so a caller could log or assert on it.
156
+ */
157
+ export declare function recordDesignIntentTelemetry(message: string, schema: unknown): DesignIntentClassification;
158
+ /**
159
+ * Record the outcome of one Jev fallback dial. This closes the measurement
160
+ * loop: the bucketing tells us how big the candidate pool is, and this tells us
161
+ * how much of it Jev actually resolves — the number that earns the flip from
162
+ * `shadow` to `enforce`.
163
+ */
164
+ export declare function recordDesignIntentJevOutcome(outcome: {
165
+ source: DesignIntentJevSource;
166
+ acted: boolean;
167
+ reason: string;
168
+ /** The classification of the send that triggered this dial. Without it the
169
+ * dial is counted, but under `unknown` rather than guessed into a shape. */
170
+ classification?: Pick<DesignIntentClassification, 'bucket' | 'shape'> | null;
171
+ }): void;
172
+ export declare function getDesignIntentTelemetrySnapshot(): DesignIntentTelemetrySnapshot;
173
+ export declare function formatDesignIntentTelemetryReport(snapshot: DesignIntentTelemetrySnapshot): string;
174
+ /** Recent events (last 100, newest last) — for a quick look in the console. */
175
+ export declare function getRecentDesignIntentEvents(): DesignIntentTelemetryEvent[];
176
+ export declare function resetDesignIntentTelemetry(): void;