vzcode 2.4.0 → 2.6.0
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/assets/{index-DuUjtrt6.js → index-Bfp1zhR4.js} +64 -64
- package/dist/assets/{index-BoFeK4GL.css → index-Bjj9VRMn.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/client/VZCodeContext/types.ts +4 -1
- package/src/client/VZCodeContext/useVZCodeState.ts +91 -3
- package/src/client/VZSidebar/AIChat/index.tsx +3 -1
- package/src/client/VZSidebar/VisualEditor.tsx +367 -118
- package/src/client/VZSidebar/styles.scss +348 -0
- package/src/types.ts +14 -3
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
useState,
|
|
5
5
|
useRef,
|
|
6
6
|
useEffect,
|
|
7
|
+
useMemo,
|
|
7
8
|
} from 'react';
|
|
8
9
|
import { VZCodeContext } from '../VZCodeContext';
|
|
9
10
|
import { VizContent, VizFileId } from '@vizhub/viz-types';
|
|
@@ -35,14 +36,19 @@ const formatValue = (
|
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
export const VisualEditor = () => {
|
|
38
|
-
const { files, submitOperation
|
|
39
|
+
const { files, submitOperation } =
|
|
39
40
|
useContext(VZCodeContext);
|
|
40
41
|
|
|
41
|
-
// Local state to track
|
|
42
|
+
// Local state to track widget values during user interaction
|
|
42
43
|
const [localValues, setLocalValues] = useState<{
|
|
43
|
-
[key: string]: number;
|
|
44
|
+
[key: string]: number | boolean | string;
|
|
44
45
|
}>({});
|
|
45
46
|
|
|
47
|
+
// State to track which dropdown is open
|
|
48
|
+
const [openDropdown, setOpenDropdown] = useState<
|
|
49
|
+
string | null
|
|
50
|
+
>(null);
|
|
51
|
+
|
|
46
52
|
let configFileId: VizFileId | null = null;
|
|
47
53
|
for (const fileId in files) {
|
|
48
54
|
if (files[fileId].name === CONFIG_FILE_NAME) {
|
|
@@ -51,47 +57,13 @@ export const VisualEditor = () => {
|
|
|
51
57
|
}
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
let configData;
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
configData = JSON.parse(files[configFileId].text);
|
|
67
|
-
} catch (error) {
|
|
68
|
-
return (
|
|
69
|
-
<EmptyState>
|
|
70
|
-
Your config.json file is not valid json.
|
|
71
|
-
</EmptyState>
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (!('visualEditorWidgets' in configData)) {
|
|
76
|
-
return (
|
|
77
|
-
<EmptyState>
|
|
78
|
-
To begin using the visual editor, make sure your
|
|
79
|
-
config.json has a key called "visualEditorWidgets",
|
|
80
|
-
whose value is the config for the visual editor.
|
|
81
|
-
</EmptyState>
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (!Array.isArray(configData.visualEditorWidgets)) {
|
|
86
|
-
return (
|
|
87
|
-
<EmptyState>
|
|
88
|
-
Your config.json file has "visualEditorWidgets" but
|
|
89
|
-
it is not an array. Please make sure
|
|
90
|
-
"visualEditorWidgets" is an array of widget
|
|
91
|
-
configurations.
|
|
92
|
-
</EmptyState>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
60
|
+
const configData = useMemo(() => {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(files[configFileId].text);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}, [files?.[configFileId]?.text]);
|
|
95
67
|
|
|
96
68
|
const onSliderChange = useCallback(
|
|
97
69
|
(property: string) =>
|
|
@@ -126,97 +98,211 @@ export const VisualEditor = () => {
|
|
|
126
98
|
[configData, files, configFileId, setLocalValues],
|
|
127
99
|
);
|
|
128
100
|
|
|
101
|
+
const onCheckboxChange = useCallback(
|
|
102
|
+
(property: string) =>
|
|
103
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
104
|
+
const newValue = event.currentTarget.checked;
|
|
105
|
+
|
|
106
|
+
// Update local state immediately for responsive UI
|
|
107
|
+
setLocalValues((prev) => ({
|
|
108
|
+
...prev,
|
|
109
|
+
[property]: newValue,
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
// Update config.json
|
|
113
|
+
const newConfigData = {
|
|
114
|
+
...configData,
|
|
115
|
+
[property]: newValue,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
submitOperation((document: VizContent) => ({
|
|
119
|
+
...document,
|
|
120
|
+
files: {
|
|
121
|
+
...files,
|
|
122
|
+
[configFileId]: {
|
|
123
|
+
name: 'config.json',
|
|
124
|
+
text: JSON.stringify(newConfigData, null, 2),
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
}));
|
|
128
|
+
},
|
|
129
|
+
[configData, files, configFileId, setLocalValues],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const onTextInputChange = useCallback(
|
|
133
|
+
(property: string) =>
|
|
134
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
135
|
+
const newValue = event.currentTarget.value;
|
|
136
|
+
|
|
137
|
+
// Update local state immediately for responsive UI
|
|
138
|
+
setLocalValues((prev) => ({
|
|
139
|
+
...prev,
|
|
140
|
+
[property]: newValue,
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
// Update config.json
|
|
144
|
+
const newConfigData = {
|
|
145
|
+
...configData,
|
|
146
|
+
[property]: newValue,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
submitOperation((document: VizContent) => ({
|
|
150
|
+
...document,
|
|
151
|
+
files: {
|
|
152
|
+
...files,
|
|
153
|
+
[configFileId]: {
|
|
154
|
+
name: 'config.json',
|
|
155
|
+
text: JSON.stringify(newConfigData, null, 2),
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
}));
|
|
159
|
+
},
|
|
160
|
+
[configData, files, configFileId, setLocalValues],
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const onDropdownChange = useCallback(
|
|
164
|
+
(property: string, newValue: string) => {
|
|
165
|
+
// Update local state immediately for responsive UI
|
|
166
|
+
setLocalValues((prev) => ({
|
|
167
|
+
...prev,
|
|
168
|
+
[property]: newValue,
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
// Update config.json
|
|
172
|
+
const newConfigData = {
|
|
173
|
+
...configData,
|
|
174
|
+
[property]: newValue,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
submitOperation((document: VizContent) => ({
|
|
178
|
+
...document,
|
|
179
|
+
files: {
|
|
180
|
+
...files,
|
|
181
|
+
[configFileId]: {
|
|
182
|
+
name: 'config.json',
|
|
183
|
+
text: JSON.stringify(newConfigData, null, 2),
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
}));
|
|
187
|
+
},
|
|
188
|
+
[configData, files, configFileId, setLocalValues],
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Custom dropdown handlers
|
|
192
|
+
const handleDropdownToggle = useCallback(
|
|
193
|
+
(property: string) => {
|
|
194
|
+
setOpenDropdown((prev) =>
|
|
195
|
+
prev === property ? null : property,
|
|
196
|
+
);
|
|
197
|
+
},
|
|
198
|
+
[],
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const handleDropdownOptionClick = useCallback(
|
|
202
|
+
(property: string, value: string) => {
|
|
203
|
+
onDropdownChange(property, value);
|
|
204
|
+
setOpenDropdown(null);
|
|
205
|
+
},
|
|
206
|
+
[onDropdownChange],
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// Close dropdown when clicking outside
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
212
|
+
if (openDropdown) {
|
|
213
|
+
const target = event.target as Element;
|
|
214
|
+
if (!target.closest('.visual-editor-dropdown')) {
|
|
215
|
+
setOpenDropdown(null);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
document.addEventListener(
|
|
221
|
+
'mousedown',
|
|
222
|
+
handleClickOutside,
|
|
223
|
+
);
|
|
224
|
+
return () => {
|
|
225
|
+
document.removeEventListener(
|
|
226
|
+
'mousedown',
|
|
227
|
+
handleClickOutside,
|
|
228
|
+
);
|
|
229
|
+
};
|
|
230
|
+
}, [openDropdown]);
|
|
231
|
+
|
|
129
232
|
const visualEditorWidgets: VisualEditorConfigEntry[] =
|
|
130
|
-
configData
|
|
233
|
+
configData?.visualEditorWidgets ?? [];
|
|
131
234
|
|
|
132
235
|
// Sync local values with config data when it changes (including remote updates)
|
|
133
236
|
useEffect(() => {
|
|
134
|
-
const newLocalValues: {
|
|
237
|
+
const newLocalValues: {
|
|
238
|
+
[key: string]: number | boolean | string;
|
|
239
|
+
} = {};
|
|
135
240
|
visualEditorWidgets.forEach((widget) => {
|
|
136
|
-
if (widget.type === '
|
|
241
|
+
if (widget.type === 'slider') {
|
|
242
|
+
newLocalValues[widget.property] =
|
|
243
|
+
configData[widget.property];
|
|
244
|
+
} else if (widget.type === 'checkbox') {
|
|
245
|
+
newLocalValues[widget.property] =
|
|
246
|
+
configData[widget.property];
|
|
247
|
+
} else if (widget.type === 'textInput') {
|
|
248
|
+
newLocalValues[widget.property] =
|
|
249
|
+
configData[widget.property];
|
|
250
|
+
} else if (widget.type === 'dropdown') {
|
|
137
251
|
newLocalValues[widget.property] =
|
|
138
252
|
configData[widget.property];
|
|
139
253
|
}
|
|
140
254
|
});
|
|
141
255
|
setLocalValues(newLocalValues);
|
|
142
|
-
}, [configData
|
|
143
|
-
|
|
144
|
-
// Track previous config state to detect changes from any source (remote clients, text editor, etc.)
|
|
145
|
-
const previousConfigRef = useRef<any>(null);
|
|
146
|
-
|
|
147
|
-
// Detect config.json changes and send updates to iframe
|
|
148
|
-
useEffect(() => {
|
|
149
|
-
if (!configFileId || !files || !files[configFileId]) {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
256
|
+
}, [configData]);
|
|
152
257
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const previousConfig = previousConfigRef.current;
|
|
162
|
-
|
|
163
|
-
// Update the ref with the new config
|
|
164
|
-
previousConfigRef.current = newConfigData;
|
|
165
|
-
|
|
166
|
-
// Skip processing if this is the first time or if there's no previous config
|
|
167
|
-
if (!previousConfig) {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
258
|
+
if (configFileId === null) {
|
|
259
|
+
return (
|
|
260
|
+
<EmptyState>
|
|
261
|
+
To begin using the visual editor, create a
|
|
262
|
+
config.json.
|
|
263
|
+
</EmptyState>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
170
266
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
typeof newConfigData[key] === 'object' &&
|
|
180
|
-
typeof previousConfig[key] === 'object'
|
|
181
|
-
) {
|
|
182
|
-
if (
|
|
183
|
-
JSON.stringify(newConfigData[key]) !==
|
|
184
|
-
JSON.stringify(previousConfig[key])
|
|
185
|
-
) {
|
|
186
|
-
changedProperties[key] = newConfigData[key];
|
|
187
|
-
}
|
|
188
|
-
} else {
|
|
189
|
-
changedProperties[key] = newConfigData[key];
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
267
|
+
if (!configData) {
|
|
268
|
+
return (
|
|
269
|
+
<EmptyState>
|
|
270
|
+
Your config.json file is not valid json.
|
|
271
|
+
</EmptyState>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
193
274
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
275
|
+
if (
|
|
276
|
+
configData &&
|
|
277
|
+
!('visualEditorWidgets' in configData)
|
|
278
|
+
) {
|
|
279
|
+
return (
|
|
280
|
+
<EmptyState>
|
|
281
|
+
To begin using the visual editor, make sure your
|
|
282
|
+
config.json has a key called "visualEditorWidgets",
|
|
283
|
+
whose value is the config for the visual editor.
|
|
284
|
+
</EmptyState>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
200
287
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}, [files, configFileId, iframeRef]);
|
|
288
|
+
if (
|
|
289
|
+
configData &&
|
|
290
|
+
!Array.isArray(configData.visualEditorWidgets)
|
|
291
|
+
) {
|
|
292
|
+
return (
|
|
293
|
+
<EmptyState>
|
|
294
|
+
Your config.json file has "visualEditorWidgets" but
|
|
295
|
+
it is not an array. Please make sure
|
|
296
|
+
"visualEditorWidgets" is an array of widget
|
|
297
|
+
configurations.
|
|
298
|
+
</EmptyState>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
215
301
|
|
|
216
302
|
return (
|
|
217
303
|
<div className="visual-editor">
|
|
218
304
|
{visualEditorWidgets.map((widgetConfig, index) => {
|
|
219
|
-
if (widgetConfig.type === '
|
|
305
|
+
if (widgetConfig.type === 'slider') {
|
|
220
306
|
// Use local value if available, otherwise fall back to config value
|
|
221
307
|
const currentValue =
|
|
222
308
|
localValues[widgetConfig.property] ??
|
|
@@ -254,7 +340,7 @@ export const VisualEditor = () => {
|
|
|
254
340
|
className="slider-input"
|
|
255
341
|
min={widgetConfig.min}
|
|
256
342
|
max={widgetConfig.max}
|
|
257
|
-
step=
|
|
343
|
+
step={widgetConfig.step}
|
|
258
344
|
value={currentValue}
|
|
259
345
|
onChange={onSliderChange(
|
|
260
346
|
widgetConfig.property,
|
|
@@ -275,6 +361,169 @@ export const VisualEditor = () => {
|
|
|
275
361
|
</div>
|
|
276
362
|
</div>
|
|
277
363
|
);
|
|
364
|
+
} else if (widgetConfig.type === 'checkbox') {
|
|
365
|
+
// Use local value if available, otherwise fall back to config value
|
|
366
|
+
const currentValue =
|
|
367
|
+
localValues[widgetConfig.property] ??
|
|
368
|
+
configData[widgetConfig.property];
|
|
369
|
+
|
|
370
|
+
return (
|
|
371
|
+
<div
|
|
372
|
+
key={widgetConfig.property}
|
|
373
|
+
className="visual-editor-checkbox"
|
|
374
|
+
>
|
|
375
|
+
<div className="checkbox-header">
|
|
376
|
+
<label
|
|
377
|
+
htmlFor={widgetConfig.property}
|
|
378
|
+
className="checkbox-label"
|
|
379
|
+
>
|
|
380
|
+
{widgetConfig.label}
|
|
381
|
+
</label>
|
|
382
|
+
<span className="checkbox-value">
|
|
383
|
+
{currentValue ? 'On' : 'Off'}
|
|
384
|
+
</span>
|
|
385
|
+
</div>
|
|
386
|
+
<div className="checkbox-container">
|
|
387
|
+
<input
|
|
388
|
+
type="checkbox"
|
|
389
|
+
id={widgetConfig.property}
|
|
390
|
+
className="checkbox-input"
|
|
391
|
+
checked={currentValue}
|
|
392
|
+
onChange={onCheckboxChange(
|
|
393
|
+
widgetConfig.property,
|
|
394
|
+
)}
|
|
395
|
+
/>
|
|
396
|
+
<div className="checkbox-visual">
|
|
397
|
+
<div
|
|
398
|
+
className={`checkbox-indicator ${
|
|
399
|
+
currentValue ? 'checked' : ''
|
|
400
|
+
}`}
|
|
401
|
+
/>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
);
|
|
406
|
+
} else if (widgetConfig.type === 'textInput') {
|
|
407
|
+
// Use local value if available, otherwise fall back to config value
|
|
408
|
+
const currentValue =
|
|
409
|
+
localValues[widgetConfig.property] ??
|
|
410
|
+
configData[widgetConfig.property];
|
|
411
|
+
|
|
412
|
+
return (
|
|
413
|
+
<div
|
|
414
|
+
key={widgetConfig.property}
|
|
415
|
+
className="visual-editor-text-input"
|
|
416
|
+
>
|
|
417
|
+
<div className="text-input-header">
|
|
418
|
+
<label
|
|
419
|
+
htmlFor={widgetConfig.property}
|
|
420
|
+
className="text-input-label"
|
|
421
|
+
>
|
|
422
|
+
{widgetConfig.label}
|
|
423
|
+
</label>
|
|
424
|
+
</div>
|
|
425
|
+
<div className="text-input-container">
|
|
426
|
+
<input
|
|
427
|
+
type="text"
|
|
428
|
+
id={widgetConfig.property}
|
|
429
|
+
className="text-input-field"
|
|
430
|
+
value={currentValue || ''}
|
|
431
|
+
onChange={onTextInputChange(
|
|
432
|
+
widgetConfig.property,
|
|
433
|
+
)}
|
|
434
|
+
/>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
);
|
|
438
|
+
} else if (widgetConfig.type === 'dropdown') {
|
|
439
|
+
// Use local value if available, otherwise fall back to config value
|
|
440
|
+
const currentValue =
|
|
441
|
+
localValues[widgetConfig.property] ??
|
|
442
|
+
configData[widgetConfig.property];
|
|
443
|
+
const isOpen =
|
|
444
|
+
openDropdown === widgetConfig.property;
|
|
445
|
+
|
|
446
|
+
return (
|
|
447
|
+
<div
|
|
448
|
+
key={widgetConfig.property}
|
|
449
|
+
className="visual-editor-dropdown"
|
|
450
|
+
>
|
|
451
|
+
<div className="dropdown-header">
|
|
452
|
+
<label
|
|
453
|
+
htmlFor={widgetConfig.property}
|
|
454
|
+
className="dropdown-label"
|
|
455
|
+
>
|
|
456
|
+
{widgetConfig.label}
|
|
457
|
+
</label>
|
|
458
|
+
{/* <span className="dropdown-value">
|
|
459
|
+
{currentValue}
|
|
460
|
+
</span> */}
|
|
461
|
+
</div>
|
|
462
|
+
<div className="dropdown-container">
|
|
463
|
+
<button
|
|
464
|
+
type="button"
|
|
465
|
+
className={`dropdown-button ${
|
|
466
|
+
isOpen ? 'open' : ''
|
|
467
|
+
}`}
|
|
468
|
+
onClick={() =>
|
|
469
|
+
handleDropdownToggle(
|
|
470
|
+
widgetConfig.property,
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
aria-expanded={isOpen}
|
|
474
|
+
aria-haspopup="listbox"
|
|
475
|
+
>
|
|
476
|
+
<span className="dropdown-button-text">
|
|
477
|
+
{currentValue}
|
|
478
|
+
</span>
|
|
479
|
+
<div
|
|
480
|
+
className={`dropdown-arrow ${
|
|
481
|
+
isOpen ? 'open' : ''
|
|
482
|
+
}`}
|
|
483
|
+
>
|
|
484
|
+
<svg
|
|
485
|
+
width="12"
|
|
486
|
+
height="8"
|
|
487
|
+
viewBox="0 0 12 8"
|
|
488
|
+
fill="none"
|
|
489
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
490
|
+
>
|
|
491
|
+
<path
|
|
492
|
+
d="M1 1.5L6 6.5L11 1.5"
|
|
493
|
+
stroke="currentColor"
|
|
494
|
+
strokeWidth="2"
|
|
495
|
+
strokeLinecap="round"
|
|
496
|
+
strokeLinejoin="round"
|
|
497
|
+
/>
|
|
498
|
+
</svg>
|
|
499
|
+
</div>
|
|
500
|
+
</button>
|
|
501
|
+
{isOpen && (
|
|
502
|
+
<div className="dropdown-options">
|
|
503
|
+
{widgetConfig.options.map((option) => (
|
|
504
|
+
<button
|
|
505
|
+
key={option}
|
|
506
|
+
type="button"
|
|
507
|
+
className={`dropdown-option ${
|
|
508
|
+
option === currentValue
|
|
509
|
+
? 'selected'
|
|
510
|
+
: ''
|
|
511
|
+
}`}
|
|
512
|
+
onClick={() =>
|
|
513
|
+
handleDropdownOptionClick(
|
|
514
|
+
widgetConfig.property,
|
|
515
|
+
option,
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
>
|
|
519
|
+
{option}
|
|
520
|
+
</button>
|
|
521
|
+
))}
|
|
522
|
+
</div>
|
|
523
|
+
)}
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
);
|
|
278
527
|
}
|
|
279
528
|
})}
|
|
280
529
|
</div>
|